From 1875542876b997bc279d754cabf29d72872307ce Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Fri, 20 Jun 2025 16:59:11 +0530 Subject: [PATCH 01/58] initial commit --- internal/cli/universal_login.go | 1 + internal/cli/universal_login_customize.go | 151 ++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/internal/cli/universal_login.go b/internal/cli/universal_login.go index 0740b5b4b..8906baaa2 100644 --- a/internal/cli/universal_login.go +++ b/internal/cli/universal_login.go @@ -64,6 +64,7 @@ func universalLoginCmd(cli *cli) *cobra.Command { cmd.SetUsageTemplate(resourceUsageTemplate()) cmd.AddCommand(customizeUniversalLoginCmd(cli)) + cmd.AddCommand(newUpdateAssetsCmd(cli)) cmd.AddCommand(switchUniversalLoginRendererModeCmd(cli)) cmd.AddCommand(showUniversalLoginCmd(cli)) cmd.AddCommand(updateUniversalLoginCmd(cli)) diff --git a/internal/cli/universal_login_customize.go b/internal/cli/universal_login_customize.go index 2a7fa6780..9a718bd62 100644 --- a/internal/cli/universal_login_customize.go +++ b/internal/cli/universal_login_customize.go @@ -7,16 +7,20 @@ import ( "errors" "fmt" "io/fs" + "log" "net" "net/http" "net/url" "os" + "path/filepath" "reflect" + "regexp" "strings" "time" "github.com/auth0/go-auth0/management" + "github.com/fsnotify/fsnotify" "github.com/gorilla/websocket" "github.com/pkg/browser" "github.com/spf13/cobra" @@ -1137,3 +1141,150 @@ func switchUniversalLoginRendererModeCmd(cli *cli) *cobra.Command { return cmd } +func newUpdateAssetsCmd(cli *cli) *cobra.Command { + var screen, prompt, watchFolder string + + cmd := &cobra.Command{ + Use: "update-assets", + Short: "Watch dist folder and patch screen assets", + RunE: func(cmd *cobra.Command, args []string) error { + return watchAndPatch(context.Background(), cli, screen, prompt, watchFolder) + }, + } + + cmd.Flags().StringVar(&screen, "screen", "", "Screen name (e.g., login)") + cmd.Flags().StringVar(&prompt, "prompt", "", "Prompt name (e.g., login)") + cmd.Flags().StringVar(&watchFolder, "watch-folder", "", "Folder to watch for new builds") + cmd.MarkFlagRequired("screen") + cmd.MarkFlagRequired("prompt") + cmd.MarkFlagRequired("watch-folder") + + return cmd +} + +func watchAndPatch(ctx context.Context, cli *cli, screen, prompt, watchFolder string) error { + if !isSupportedPartial(management.PromptType(prompt)) { + return fmt.Errorf("the prompt %q is not supported for partials", prompt) + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return fmt.Errorf("failed to create file watcher: %w", err) + } + + defer watcher.Close() + + err = watcher.Add(watchFolder) + if err != nil { + return fmt.Errorf("failed to add folder to watcher: %w", err) + } + + fmt.Printf("Watching folder %q for changes...\n", watchFolder) + + settings, err := fetchSettings(ctx, cli, prompt, screen) + if err != nil { + return fmt.Errorf("failed to fetch settings for prompt %q and screen %q: %w", prompt, screen, err) + } + + if settings == nil { + return fmt.Errorf("no settings found for prompt %q and screen %q", prompt, screen) + } + + fmt.Println(settings.HeadTags) + + for { + select { + case event, ok := <-watcher.Events: + if !ok || event.Op&fsnotify.Create == 0 { + continue + } + + if strings.HasSuffix(event.Name, ".js") || strings.HasSuffix(event.Name, ".css") { + time.Sleep(500 * time.Millisecond) // wait for file to stabilize + log.Println("Change detected:", event.Name) + + // Get latest .js and .css files + jsFile, cssFile, err := getLatestAssets(watchFolder) + if err != nil { + log.Println("Error:", err) + continue + } + + fmt.Printf("Latest assets found: %s, %s\n", jsFile, cssFile) + + settings.HeadTags = []interface{}{ + map[string]interface{}{ + "tag": "script", + "attributes": map[string]interface{}{ + "defer": true, + "async": true, + "src": jsFile, + //"integrity": []string{jsHash}, + }, + }, + map[string]interface{}{ + "tag": "link", + "attributes": map[string]interface{}{ + "href": cssFile, + "rel": "stylesheet", + }, + }, + } + + // Patch settings to Auth0 + if err := patchToAuth0(ctx, cli, prompt, screen, settings); err != nil { + log.Println("Patch error:", err) + } else { + log.Println("Patch successful.") + } + } + case err := <-watcher.Errors: + fmt.Printf("Error: %v\n", err) + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func getLatestAssets(folder string) (jsFile, cssFile string, err error) { + var jsPattern = regexp.MustCompile(`^index-[a-f0-9]+\.js$`) + var cssPattern = regexp.MustCompile(`^index-[a-f0-9]+\.css$`) + + err = filepath.WalkDir(folder, func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil || d.IsDir() { + return nil + } + + base := filepath.Base(path) + switch { + case jsPattern.MatchString(base): + jsFile = path + case cssPattern.MatchString(base): + cssFile = path + } + + return nil + }) + + if jsFile == "" || cssFile == "" { + return "", "", fmt.Errorf("missing required asset files") + } + + return jsFile, cssFile, nil +} + +func fetchSettings(ctx context.Context, cli *cli, promptName, screenName string) (*management.PromptRendering, error) { + return cli.api.Prompt.ReadRendering(ctx, management.PromptType(promptName), management.ScreenName(screenName)) +} + +func patchToAuth0(ctx context.Context, cli *cli, promptName, screenName string, settings *management.PromptRendering) error { + if settings == nil || settings.RenderingMode == nil { + return fmt.Errorf("settings or rendering mode is nil") + } + + if err := cli.api.Prompt.UpdateRendering(ctx, management.PromptType(promptName), management.ScreenName(screenName), settings); err != nil { + return fmt.Errorf("failed to patch settings to Auth0: %w", err) + } + + return nil +} From 06d9748e9cda63bf7f9ce732f03efdf0530a12d9 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 23 Jun 2025 08:27:35 +0530 Subject: [PATCH 02/58] add logic to read .js and .css from all the folders in assets for a given screen --- internal/cli/universal_login_customize.go | 168 +++++++++++----------- 1 file changed, 88 insertions(+), 80 deletions(-) diff --git a/internal/cli/universal_login_customize.go b/internal/cli/universal_login_customize.go index 9a718bd62..27b61fefa 100644 --- a/internal/cli/universal_login_customize.go +++ b/internal/cli/universal_login_customize.go @@ -14,7 +14,6 @@ import ( "os" "path/filepath" "reflect" - "regexp" "strings" "time" @@ -1142,44 +1141,41 @@ func switchUniversalLoginRendererModeCmd(cli *cli) *cobra.Command { return cmd } func newUpdateAssetsCmd(cli *cli) *cobra.Command { - var screen, prompt, watchFolder string + var screen, prompt, watchFolder, assetURL string cmd := &cobra.Command{ Use: "update-assets", Short: "Watch dist folder and patch screen assets", RunE: func(cmd *cobra.Command, args []string) error { - return watchAndPatch(context.Background(), cli, screen, prompt, watchFolder) + return watchAndPatch(context.Background(), cli, screen, prompt, assetURL, watchFolder) }, } cmd.Flags().StringVar(&screen, "screen", "", "Screen name (e.g., login)") cmd.Flags().StringVar(&prompt, "prompt", "", "Prompt name (e.g., login)") cmd.Flags().StringVar(&watchFolder, "watch-folder", "", "Folder to watch for new builds") + cmd.Flags().StringVar(&assetURL, "assets-url", "", "Base URL for serving dist assets (e.g., http://localhost:5173)") cmd.MarkFlagRequired("screen") cmd.MarkFlagRequired("prompt") cmd.MarkFlagRequired("watch-folder") + cmd.MarkFlagRequired("asset-url") return cmd } -func watchAndPatch(ctx context.Context, cli *cli, screen, prompt, watchFolder string) error { - if !isSupportedPartial(management.PromptType(prompt)) { - return fmt.Errorf("the prompt %q is not supported for partials", prompt) - } - +func watchAndPatch(ctx context.Context, cli *cli, screen, prompt, assetsUrl, watchFolder string) error { watcher, err := fsnotify.NewWatcher() if err != nil { - return fmt.Errorf("failed to create file watcher: %w", err) + return err } defer watcher.Close() - err = watcher.Add(watchFolder) - if err != nil { - return fmt.Errorf("failed to add folder to watcher: %w", err) + if err := watcher.Add(watchFolder); err != nil { + return fmt.Errorf("failed to watch %s: %w", watchFolder, err) } - fmt.Printf("Watching folder %q for changes...\n", watchFolder) + fmt.Printf("Watching folder '%q' for assets changes...\n", watchFolder) settings, err := fetchSettings(ctx, cli, prompt, screen) if err != nil { @@ -1190,52 +1186,42 @@ func watchAndPatch(ctx context.Context, cli *cli, screen, prompt, watchFolder st return fmt.Errorf("no settings found for prompt %q and screen %q", prompt, screen) } - fmt.Println(settings.HeadTags) + fmt.Println("previous: ", settings.HeadTags) + + var lastEventTime time.Time + const debounceDelay = 500 * time.Millisecond for { select { case event, ok := <-watcher.Events: - if !ok || event.Op&fsnotify.Create == 0 { - continue + if !ok { + return nil } - if strings.HasSuffix(event.Name, ".js") || strings.HasSuffix(event.Name, ".css") { - time.Sleep(500 * time.Millisecond) // wait for file to stabilize - log.Println("Change detected:", event.Name) + if strings.HasSuffix(event.Name, "assets") { + if event.Op&(fsnotify.Create|fsnotify.Remove|fsnotify.Rename) != 0 { + // Debounce events + if time.Since(lastEventTime) < debounceDelay { + continue + } + lastEventTime = time.Now() + log.Println("Change detected in assets:", event.Name) - // Get latest .js and .css files - jsFile, cssFile, err := getLatestAssets(watchFolder) - if err != nil { - log.Println("Error:", err) - continue - } + headTags, err := buildHeadTagsFromDist(watchFolder, assetsUrl) + if err != nil { + log.Printf("Failed to build head tags: %v\n", err) + continue + } - fmt.Printf("Latest assets found: %s, %s\n", jsFile, cssFile) + settings.HeadTags = headTags - settings.HeadTags = []interface{}{ - map[string]interface{}{ - "tag": "script", - "attributes": map[string]interface{}{ - "defer": true, - "async": true, - "src": jsFile, - //"integrity": []string{jsHash}, - }, - }, - map[string]interface{}{ - "tag": "link", - "attributes": map[string]interface{}{ - "href": cssFile, - "rel": "stylesheet", - }, - }, - } - - // Patch settings to Auth0 - if err := patchToAuth0(ctx, cli, prompt, screen, settings); err != nil { - log.Println("Patch error:", err) - } else { - log.Println("Patch successful.") + // Patch settings to Auth0 + if err := updateSettings(ctx, cli, prompt, screen, settings); err != nil { + log.Println("Patch error:", err) + } else { + log.Println("Patch successful.") + } + //}() } } case err := <-watcher.Errors: @@ -1246,45 +1232,67 @@ func watchAndPatch(ctx context.Context, cli *cli, screen, prompt, watchFolder st } } -func getLatestAssets(folder string) (jsFile, cssFile string, err error) { - var jsPattern = regexp.MustCompile(`^index-[a-f0-9]+\.js$`) - var cssPattern = regexp.MustCompile(`^index-[a-f0-9]+\.css$`) - - err = filepath.WalkDir(folder, func(path string, d fs.DirEntry, walkErr error) error { - if walkErr != nil || d.IsDir() { - return nil - } - - base := filepath.Base(path) - switch { - case jsPattern.MatchString(base): - jsFile = path - case cssPattern.MatchString(base): - cssFile = path - } - - return nil - }) - - if jsFile == "" || cssFile == "" { - return "", "", fmt.Errorf("missing required asset files") - } - - return jsFile, cssFile, nil -} - func fetchSettings(ctx context.Context, cli *cli, promptName, screenName string) (*management.PromptRendering, error) { return cli.api.Prompt.ReadRendering(ctx, management.PromptType(promptName), management.ScreenName(screenName)) } -func patchToAuth0(ctx context.Context, cli *cli, promptName, screenName string, settings *management.PromptRendering) error { +func updateSettings(ctx context.Context, cli *cli, promptName, screenName string, settings *management.PromptRendering) error { if settings == nil || settings.RenderingMode == nil { return fmt.Errorf("settings or rendering mode is nil") } - if err := cli.api.Prompt.UpdateRendering(ctx, management.PromptType(promptName), management.ScreenName(screenName), settings); err != nil { - return fmt.Errorf("failed to patch settings to Auth0: %w", err) + if err := ansi.Waiting(func() error { + return cli.api.Prompt.UpdateRendering(ctx, management.PromptType(promptName), management.ScreenName(screenName), settings) + }); err != nil { + return fmt.Errorf("failed to set the render settings: %w", err) } return nil } + +func buildHeadTagsFromDist(distDir, assetURLPrefix string) ([]interface{}, error) { + var headTags []interface{} + + targetFolder := filepath.Join(distDir, "assets") + + err := filepath.Walk(targetFolder, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + + ext := filepath.Ext(path) + relPath, err := filepath.Rel(distDir, path) + if err != nil { + return err + } + + relPath = filepath.ToSlash(relPath) + fullURL := strings.TrimRight(assetURLPrefix, "/") + "/" + relPath + + switch ext { + case ".js": + headTags = append(headTags, map[string]interface{}{ + "tag": "script", + "attributes": map[string]interface{}{ + "src": fullURL, + "defer": true, + }, + }) + case ".css": + headTags = append(headTags, map[string]interface{}{ + "tag": "link", + "attributes": map[string]interface{}{ + "href": fullURL, + "rel": "stylesheet", + }, + }) + } + return nil + }) + + if err != nil { + return nil, fmt.Errorf("error reading folder %s: %w", targetFolder, err) + } + + return headTags, nil +} From f0c4e4389ef60484c27044ac5b13a23ac041444e Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 23 Jun 2025 09:44:40 +0530 Subject: [PATCH 03/58] fix logic by adding a sleep timer to settle the files and update command --- internal/cli/universal_login_customize.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/internal/cli/universal_login_customize.go b/internal/cli/universal_login_customize.go index 27b61fefa..d7c091a42 100644 --- a/internal/cli/universal_login_customize.go +++ b/internal/cli/universal_login_customize.go @@ -1144,7 +1144,7 @@ func newUpdateAssetsCmd(cli *cli) *cobra.Command { var screen, prompt, watchFolder, assetURL string cmd := &cobra.Command{ - Use: "update-assets", + Use: "watch-assets", Short: "Watch dist folder and patch screen assets", RunE: func(cmd *cobra.Command, args []string) error { return watchAndPatch(context.Background(), cli, screen, prompt, assetURL, watchFolder) @@ -1186,10 +1186,10 @@ func watchAndPatch(ctx context.Context, cli *cli, screen, prompt, assetsUrl, wat return fmt.Errorf("no settings found for prompt %q and screen %q", prompt, screen) } - fmt.Println("previous: ", settings.HeadTags) + fmt.Println("Initial settings: ", settings.HeadTags) - var lastEventTime time.Time - const debounceDelay = 500 * time.Millisecond + //var lastEventTime time.Time + const debounceDelay = 2000 * time.Millisecond for { select { @@ -1199,12 +1199,9 @@ func watchAndPatch(ctx context.Context, cli *cli, screen, prompt, assetsUrl, wat } if strings.HasSuffix(event.Name, "assets") { - if event.Op&(fsnotify.Create|fsnotify.Remove|fsnotify.Rename) != 0 { - // Debounce events - if time.Since(lastEventTime) < debounceDelay { - continue - } - lastEventTime = time.Now() + if event.Op&(fsnotify.Create) != 0 { + time.Sleep(300 * time.Millisecond) // Allow some time for the file system to settle + log.Println("Change detected in assets:", event.Name) headTags, err := buildHeadTagsFromDist(watchFolder, assetsUrl) @@ -1221,7 +1218,6 @@ func watchAndPatch(ctx context.Context, cli *cli, screen, prompt, assetsUrl, wat } else { log.Println("Patch successful.") } - //}() } } case err := <-watcher.Errors: From feb7a7a7182d6c2d5b8a89978d3c8efb748b7633 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 23 Jun 2025 12:00:12 +0530 Subject: [PATCH 04/58] Update head_tags content for .js extended files --- internal/cli/universal_login_customize.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/cli/universal_login_customize.go b/internal/cli/universal_login_customize.go index d7c091a42..5a9333b9c 100644 --- a/internal/cli/universal_login_customize.go +++ b/internal/cli/universal_login_customize.go @@ -1189,7 +1189,7 @@ func watchAndPatch(ctx context.Context, cli *cli, screen, prompt, assetsUrl, wat fmt.Println("Initial settings: ", settings.HeadTags) //var lastEventTime time.Time - const debounceDelay = 2000 * time.Millisecond + //const debounceDelay = 2000 * time.Millisecond for { select { @@ -1272,6 +1272,7 @@ func buildHeadTagsFromDist(distDir, assetURLPrefix string) ([]interface{}, error "attributes": map[string]interface{}{ "src": fullURL, "defer": true, + "type": "module", }, }) case ".css": From 8eafe6d490e192353d50d11fab5e457ab033d764 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Wed, 9 Jul 2025 22:15:03 +0530 Subject: [PATCH 05/58] Update logic to support watching screens 1 or all or specific --- internal/cli/universal_login_customize.go | 408 ++++++++++++++++------ 1 file changed, 309 insertions(+), 99 deletions(-) diff --git a/internal/cli/universal_login_customize.go b/internal/cli/universal_login_customize.go index 5a9333b9c..5388ab6c4 100644 --- a/internal/cli/universal_login_customize.go +++ b/internal/cli/universal_login_customize.go @@ -15,6 +15,7 @@ import ( "path/filepath" "reflect" "strings" + "sync" "time" "github.com/auth0/go-auth0/management" @@ -97,6 +98,15 @@ var ( Help: "Script contents for the rendering configs.", IsRequired: true, } + + screens1 = Flag{ + Name: "screen", + LongForm: "screens", + ShortForm: "s", + Help: "watching screens", + IsRequired: false, + AlwaysPrompt: true, + } ) var allowedPromptsWithPartials = []management.PromptType{ @@ -109,7 +119,7 @@ var allowedPromptsWithPartials = []management.PromptType{ management.PromptLoginPasswordLess, } -var ScreenPromptMap = map[string][]string{ +var PromptScreenMap = map[string][]string{ "signup-id": {"signup-id"}, "signup-password": {"signup-password"}, "login-id": {"login-id"}, @@ -145,6 +155,81 @@ var ScreenPromptMap = map[string][]string{ "mfa-webauthn-platform-challenge", "mfa-webauthn-platform-enrollment", "mfa-webauthn-roaming-challenge", "mfa-webauthn-roaming-enrollment"}, } +var ScreenPromptMap = map[string]string{ + "signup-id": "signup-id", + "signup-password": "signup-password", + "login-id": "login-id", + "login-password": "login-password", + "login-passwordless-email-code": "login-passwordless", + "login-passwordless-sms-otp": "login-passwordless", + "phone-identifier-enrollment": "phone-identifier-enrollment", + "phone-identifier-challenge": "phone-identifier-challenge", + "email-identifier-challenge": "email-identifier-challenge", + "passkey-enrollment": "passkeys", + "passkey-enrollment-local": "passkeys", + "interstitial-captcha": "captcha", + "login": "login", + "signup": "signup", + "reset-password-request": "reset-password", + "reset-password-email": "reset-password", + "reset-password": "reset-password", + "reset-password-success": "reset-password", + "reset-password-error": "reset-password", + "reset-password-mfa-email-challenge": "reset-password", + "reset-password-mfa-otp-challenge": "reset-password", + "reset-password-mfa-push-challenge-push": "reset-password", + "reset-password-mfa-sms-challenge": "reset-password", + "reset-password-mfa-phone-challenge": "reset-password", + "reset-password-mfa-voice-challenge": "reset-password", + "reset-password-mfa-recovery-code-challenge": "reset-password", + "reset-password-mfa-webauthn-platform-challenge": "reset-password", + "reset-password-mfa-webauthn-roaming-challenge": "reset-password", + "mfa-detect-browser-capabilities": "mfa", + "mfa-enroll-result": "mfa", + "mfa-begin-enroll-options": "mfa", + "mfa-login-options": "mfa", + "mfa-email-challenge": "mfa-email", + "mfa-email-list": "mfa-email", + "mfa-country-codes": "mfa-sms", + "mfa-sms-challenge": "mfa-sms", + "mfa-sms-enrollment": "mfa-sms", + "mfa-sms-list": "mfa-sms", + "mfa-push-challenge-push": "mfa-push", + "mfa-push-enrollment-qr": "mfa-push", + "mfa-push-list": "mfa-push", + "mfa-push-welcome": "mfa-push", + "accept-invitation": "invitation", + "organization-selection": "organizations", + "organization-picker": "organizations", + "mfa-otp-challenge": "mfa-otp", + "mfa-otp-enrollment-code": "mfa-otp", + "mfa-otp-enrollment-qr": "mfa-otp", + "device-code-activation": "device-flow", + "device-code-activation-allowed": "device-flow", + "device-code-activation-denied": "device-flow", + "device-code-confirmation": "device-flow", + "mfa-phone-challenge": "mfa-phone", + "mfa-phone-enrollment": "mfa-phone", + "mfa-voice-challenge": "mfa-voice", + "mfa-voice-enrollment": "mfa-voice", + "mfa-recovery-code-challenge": "mfa-recovery-code", + "mfa-recovery-code-enrollment": "mfa-recovery-code", + "mfa-recovery-code-challenge-new-code": "mfa-recovery-code", + "redeem-ticket": "common", + "email-verification-result": "email-verification", + "login-email-verification": "login-email-verification", + "logout": "logout", + "logout-aborted": "logout", + "logout-complete": "logout", + "mfa-webauthn-change-key-nickname": "mfa-webauthn", + "mfa-webauthn-enrollment-success": "mfa-webauthn", + "mfa-webauthn-error": "mfa-webauthn", + "mfa-webauthn-platform-challenge": "mfa-webauthn", + "mfa-webauthn-platform-enrollment": "mfa-webauthn", + "mfa-webauthn-roaming-challenge": "mfa-webauthn", + "mfa-webauthn-roaming-enrollment": "mfa-webauthn", +} + type partialsData map[string]*management.PromptScreenPartials type ( @@ -383,19 +468,19 @@ func advanceCustomize(cmd *cobra.Command, cli *cli, input customizationInputs) e func fetchPromptScreenInfo(cmd *cobra.Command, cli *cli, input *promptScreen, action string) error { if input.promptName == "" { cli.renderer.Infof("Please select a prompt to %s its rendering mode:", action) - if err := promptName.Select(cmd, &input.promptName, utils.FetchKeys(ScreenPromptMap), nil); err != nil { + if err := promptName.Select(cmd, &input.promptName, utils.FetchKeys(PromptScreenMap), nil); err != nil { return handleInputError(err) } } if input.screenName == "" { - if len(ScreenPromptMap[input.promptName]) > 1 { + if len(PromptScreenMap[input.promptName]) > 1 { cli.renderer.Infof("Please select a screen to %s its rendering mode:", action) - if err := screenName.Select(cmd, &input.screenName, ScreenPromptMap[input.promptName], nil); err != nil { + if err := screenName.Select(cmd, &input.screenName, PromptScreenMap[input.promptName], nil); err != nil { return handleInputError(err) } } else { - input.screenName = ScreenPromptMap[input.promptName][0] + input.screenName = PromptScreenMap[input.promptName][0] } } @@ -1141,29 +1226,63 @@ func switchUniversalLoginRendererModeCmd(cli *cli) *cobra.Command { return cmd } func newUpdateAssetsCmd(cli *cli) *cobra.Command { - var screen, prompt, watchFolder, assetURL string + var watchFolder, assetURL string + var screens []string cmd := &cobra.Command{ Use: "watch-assets", Short: "Watch dist folder and patch screen assets", RunE: func(cmd *cobra.Command, args []string) error { - return watchAndPatch(context.Background(), cli, screen, prompt, assetURL, watchFolder) + return watchAndPatch(context.Background(), cli, assetURL, watchFolder, screens) }, } - cmd.Flags().StringVar(&screen, "screen", "", "Screen name (e.g., login)") - cmd.Flags().StringVar(&prompt, "prompt", "", "Prompt name (e.g., login)") + screens1.RegisterStringSlice(cmd, &screens, nil) cmd.Flags().StringVar(&watchFolder, "watch-folder", "", "Folder to watch for new builds") cmd.Flags().StringVar(&assetURL, "assets-url", "", "Base URL for serving dist assets (e.g., http://localhost:5173)") - cmd.MarkFlagRequired("screen") - cmd.MarkFlagRequired("prompt") + cmd.MarkFlagRequired("screens1") cmd.MarkFlagRequired("watch-folder") cmd.MarkFlagRequired("asset-url") return cmd } -func watchAndPatch(ctx context.Context, cli *cli, screen, prompt, assetsUrl, watchFolder string) error { +func fetchSettings(ctx context.Context, cli *cli, promptName, screenName string) (*management.PromptRendering, error) { + return cli.api.Prompt.ReadRendering(ctx, management.PromptType(promptName), management.ScreenName(screenName)) +} + +func updateSettings(ctx context.Context, cli *cli, screenName string, settings *management.PromptRendering) error { + if settings == nil || settings.RenderingMode == nil { + return fmt.Errorf("settings or rendering mode is nil") + } + + if err := ansi.Waiting(func() error { + return cli.api.Prompt.UpdateRendering(ctx, management.PromptType(ScreenPromptMap[screenName]), management.ScreenName(screenName), settings) + }); err != nil { + return fmt.Errorf("failed to set the render settings: %w", err) + } + + return nil +} + +func patchScreen(ctx context.Context, cli *cli, prompt, assetsURL, distAssetsPath, screen string) error { + settings, err := fetchSettings(ctx, cli, prompt, screen) + if err != nil || settings == nil { + return fmt.Errorf("failed to fetch settings for %s: %w", screen, err) + } + headTags, err := buildHeadTagsFromDirs(filepath.Dir(distAssetsPath), assetsURL, screen) + if err != nil { + return fmt.Errorf("failed to build head tags for %s: %w", screen, err) + } + settings.HeadTags = headTags + if err := updateSettings(ctx, cli, screen, settings); err != nil { + return fmt.Errorf("failed to update settings for %s: %w", screen, err) + } + log.Printf("✅ Patched screen: %s", screen) + return nil +} + +func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screenDirs []string) error { watcher, err := fsnotify.NewWatcher() if err != nil { return err @@ -1171,125 +1290,216 @@ func watchAndPatch(ctx context.Context, cli *cli, screen, prompt, assetsUrl, wat defer watcher.Close() - if err := watcher.Add(watchFolder); err != nil { - return fmt.Errorf("failed to watch %s: %w", watchFolder, err) + distAssetsPath := filepath.Join(distPath, "assets") + var screensToWatch []string + + if len(screenDirs) == 1 && screenDirs[0] == "all" { + dirs, err := os.ReadDir(distAssetsPath) + if err != nil { + return fmt.Errorf("failed to read assets dir: %w", err) + } + for _, d := range dirs { + if d.IsDir() && d.Name() != "shared" { + screensToWatch = append(screensToWatch, d.Name()) + } + } + } else { + for _, screen := range screenDirs { + path := filepath.Join(distAssetsPath, screen) + _, err = os.Stat(path) + if err != nil { + log.Printf("⚠️ screen directory %q not found in dist/assets: %v", screen, err) + continue + } + + screensToWatch = append(screensToWatch, screen) + } } - fmt.Printf("Watching folder '%q' for assets changes...\n", watchFolder) + screenSet := make(map[string]bool) + for _, s := range screensToWatch { + screenSet[s] = true + } - settings, err := fetchSettings(ctx, cli, prompt, screen) - if err != nil { - return fmt.Errorf("failed to fetch settings for prompt %q and screen %q: %w", prompt, screen, err) + // Watch paths + watchPaths := []string{ + // distPath, + distAssetsPath, + filepath.Join(distAssetsPath, "shared"), + } + + for _, screen := range screensToWatch { + watchPaths = append(watchPaths, filepath.Join(distAssetsPath, screen)) + } + + var watchedPaths []string + for _, path := range watchPaths { + if err := watcher.Add(path); err != nil { + log.Printf("⚠️ Failed to watch %q: %v", path, err) + } else { + watchedPaths = append(watchedPaths, filepath.Base(path)) + } } - if settings == nil { - return fmt.Errorf("no settings found for prompt %q and screen %q", prompt, screen) + if len(watchedPaths) > 0 { + log.Printf("👀 Watching %d folders: %s", len(watchedPaths), strings.Join(watchedPaths, ", ")) } - fmt.Println("Initial settings: ", settings.HeadTags) + eventChan := make(chan string, 100) - //var lastEventTime time.Time - //const debounceDelay = 2000 * time.Millisecond + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } - for { - select { - case event, ok := <-watcher.Events: + if event.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Rename|fsnotify.Remove) != 0 && isAssetFile(event.Name) { + eventChan <- event.Name + } + case err := <-watcher.Errors: + log.Printf("❗ Watcher error: %v", err) + case <-ctx.Done(): + return + } + } + }() + + // Start a goroutine to process file change events in batches + go func() { + for { + // Wait for the first event from the channel + path, ok := <-eventChan if !ok { - return nil + return } - if strings.HasSuffix(event.Name, "assets") { - if event.Op&(fsnotify.Create) != 0 { - time.Sleep(300 * time.Millisecond) // Allow some time for the file system to settle + paths := []string{path} - log.Println("Change detected in assets:", event.Name) + // Set a 2-second window to collect additional events + collectWindow := time.After(2 * time.Second) - headTags, err := buildHeadTagsFromDist(watchFolder, assetsUrl) - if err != nil { - log.Printf("Failed to build head tags: %v\n", err) - continue - } + loop: + for { + select { + // If another file change event comes in before 2s, collect it + case p := <-eventChan: + paths = append(paths, p) - settings.HeadTags = headTags + // If no events arrive within the window, break and process collected paths + case <-collectWindow: + break loop + } + } - // Patch settings to Auth0 - if err := updateSettings(ctx, cli, prompt, screen, settings); err != nil { - log.Println("Patch error:", err) - } else { - log.Println("Patch successful.") + affectedScreens := make(map[string]bool) + sharedChanged := false + + for _, p := range paths { + relPath, _ := filepath.Rel(distAssetsPath, p) + relPath = filepath.ToSlash(relPath) + + switch { + case strings.HasPrefix(relPath, "shared/"), strings.HasPrefix(relPath, "main-") && strings.HasSuffix(relPath, ".js"): + sharedChanged = true + break + + default: + parts := strings.Split(relPath, "/") + if len(parts) >= 2 { + screen := parts[0] + if screenSet[screen] { + affectedScreens[screen] = true + } else { + log.Printf("ℹ️ Ignored change from unknown or untracked folder: %q", screen) + } } } } - case err := <-watcher.Errors: - fmt.Printf("Error: %v\n", err) - case <-ctx.Done(): - return ctx.Err() - } - } -} -func fetchSettings(ctx context.Context, cli *cli, promptName, screenName string) (*management.PromptRendering, error) { - return cli.api.Prompt.ReadRendering(ctx, management.PromptType(promptName), management.ScreenName(screenName)) -} + var wg sync.WaitGroup + errChan := make(chan error, len(screensToWatch)) + + if sharedChanged { + for _, screen := range screensToWatch { + wg.Add(1) + go func(screen string) { + defer wg.Done() + if err = patchScreen(ctx, cli, ScreenPromptMap[screen], assetsURL, distAssetsPath, screen); err != nil { + errChan <- fmt.Errorf("❌ Failed to patch screen %q: %v", screen, err) + } + }(screen) + } + } else { + for screen := range affectedScreens { + wg.Add(1) + go func(screen string) { + defer wg.Done() + if err = patchScreen(ctx, cli, ScreenPromptMap[screen], assetsURL, distAssetsPath, screen); err != nil { + errChan <- fmt.Errorf("❌ Failed to patch screen %q: %v", screen, err) + } + }(screen) + } + } -func updateSettings(ctx context.Context, cli *cli, promptName, screenName string, settings *management.PromptRendering) error { - if settings == nil || settings.RenderingMode == nil { - return fmt.Errorf("settings or rendering mode is nil") - } + wg.Wait() + close(errChan) - if err := ansi.Waiting(func() error { - return cli.api.Prompt.UpdateRendering(ctx, management.PromptType(promptName), management.ScreenName(screenName), settings) - }); err != nil { - return fmt.Errorf("failed to set the render settings: %w", err) - } + for err = range errChan { + log.Println(err) + } + } + }() + <-ctx.Done() return nil } -func buildHeadTagsFromDist(distDir, assetURLPrefix string) ([]interface{}, error) { - var headTags []interface{} +func isAssetFile(name string) bool { + return strings.HasSuffix(name, ".js") || strings.HasSuffix(name, ".css") +} - targetFolder := filepath.Join(distDir, "assets") +func buildHeadTagsFromDirs(distPath, assetsURL, screen string) ([]interface{}, error) { + var tags []interface{} + screenPath := filepath.Join(distPath, "assets", screen) + sharedPath := filepath.Join(distPath, "assets", "shared") + mainPath := filepath.Join(distPath, "assets") - err := filepath.Walk(targetFolder, func(path string, info os.FileInfo, err error) error { - if err != nil || info.IsDir() { - return nil - } + sources := []string{sharedPath, screenPath, mainPath} - ext := filepath.Ext(path) - relPath, err := filepath.Rel(distDir, path) + for _, dir := range sources { + entries, err := os.ReadDir(dir) if err != nil { - return err + continue // skip on error } - - relPath = filepath.ToSlash(relPath) - fullURL := strings.TrimRight(assetURLPrefix, "/") + "/" + relPath - - switch ext { - case ".js": - headTags = append(headTags, map[string]interface{}{ - "tag": "script", - "attributes": map[string]interface{}{ - "src": fullURL, - "defer": true, - "type": "module", - }, - }) - case ".css": - headTags = append(headTags, map[string]interface{}{ - "tag": "link", - "attributes": map[string]interface{}{ - "href": fullURL, - "rel": "stylesheet", - }, - }) + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + src := fmt.Sprintf("%s/assets/%s/%s", assetsURL, filepath.Base(dir), name) + + switch { + case strings.HasSuffix(name, ".js"): + tags = append(tags, map[string]interface{}{ + "tag": "script", + "attributes": map[string]interface{}{ + "src": src, + "defer": true, + }, + }) + case strings.HasSuffix(name, ".css"): + tags = append(tags, map[string]interface{}{ + "tag": "link", + "attributes": map[string]interface{}{ + "href": src, + "rel": "stylesheet", + }, + }) + } } - return nil - }) - - if err != nil { - return nil, fmt.Errorf("error reading folder %s: %w", targetFolder, err) } - return headTags, nil + return tags, nil } From ec56478531d8e03290b8b7c867b0dfdb49df6b21 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Fri, 11 Jul 2025 07:53:40 +0530 Subject: [PATCH 06/58] Update logic to support watching screens --- internal/cli/universal_login_customize.go | 176 +++++++--------------- 1 file changed, 51 insertions(+), 125 deletions(-) diff --git a/internal/cli/universal_login_customize.go b/internal/cli/universal_login_customize.go index 5388ab6c4..7f4126b09 100644 --- a/internal/cli/universal_login_customize.go +++ b/internal/cli/universal_login_customize.go @@ -1247,12 +1247,8 @@ func newUpdateAssetsCmd(cli *cli) *cobra.Command { return cmd } -func fetchSettings(ctx context.Context, cli *cli, promptName, screenName string) (*management.PromptRendering, error) { - return cli.api.Prompt.ReadRendering(ctx, management.PromptType(promptName), management.ScreenName(screenName)) -} - func updateSettings(ctx context.Context, cli *cli, screenName string, settings *management.PromptRendering) error { - if settings == nil || settings.RenderingMode == nil { + if settings == nil { return fmt.Errorf("settings or rendering mode is nil") } @@ -1265,29 +1261,11 @@ func updateSettings(ctx context.Context, cli *cli, screenName string, settings * return nil } -func patchScreen(ctx context.Context, cli *cli, prompt, assetsURL, distAssetsPath, screen string) error { - settings, err := fetchSettings(ctx, cli, prompt, screen) - if err != nil || settings == nil { - return fmt.Errorf("failed to fetch settings for %s: %w", screen, err) - } - headTags, err := buildHeadTagsFromDirs(filepath.Dir(distAssetsPath), assetsURL, screen) - if err != nil { - return fmt.Errorf("failed to build head tags for %s: %w", screen, err) - } - settings.HeadTags = headTags - if err := updateSettings(ctx, cli, screen, settings); err != nil { - return fmt.Errorf("failed to update settings for %s: %w", screen, err) - } - log.Printf("✅ Patched screen: %s", screen) - return nil -} - func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screenDirs []string) error { watcher, err := fsnotify.NewWatcher() if err != nil { return err } - defer watcher.Close() distAssetsPath := filepath.Join(distPath, "assets") @@ -1298,6 +1276,7 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc if err != nil { return fmt.Errorf("failed to read assets dir: %w", err) } + for _, d := range dirs { if d.IsDir() && d.Name() != "shared" { screensToWatch = append(screensToWatch, d.Name()) @@ -1306,43 +1285,23 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } else { for _, screen := range screenDirs { path := filepath.Join(distAssetsPath, screen) - _, err = os.Stat(path) + info, err := os.Stat(path) if err != nil { log.Printf("⚠️ screen directory %q not found in dist/assets: %v", screen, err) continue } - + if !info.IsDir() { + log.Printf("⚠️ screen path %q exists but is not a directory", path) + continue + } screensToWatch = append(screensToWatch, screen) } } - screenSet := make(map[string]bool) - for _, s := range screensToWatch { - screenSet[s] = true - } - - // Watch paths - watchPaths := []string{ - // distPath, - distAssetsPath, - filepath.Join(distAssetsPath, "shared"), - } - - for _, screen := range screensToWatch { - watchPaths = append(watchPaths, filepath.Join(distAssetsPath, screen)) - } - - var watchedPaths []string - for _, path := range watchPaths { - if err := watcher.Add(path); err != nil { - log.Printf("⚠️ Failed to watch %q: %v", path, err) - } else { - watchedPaths = append(watchedPaths, filepath.Base(path)) - } - } - - if len(watchedPaths) > 0 { - log.Printf("👀 Watching %d folders: %s", len(watchedPaths), strings.Join(watchedPaths, ", ")) + if err := watcher.Add(distPath); err != nil { + log.Printf("⚠️ Failed to watch %q: %v", distPath, err) + } else { + log.Printf("👀 Watching: %d screen(s): %v", len(screensToWatch), screensToWatch) } eventChan := make(chan string, 100) @@ -1354,8 +1313,8 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc if !ok { return } - - if event.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Rename|fsnotify.Remove) != 0 && isAssetFile(event.Name) { + if strings.HasSuffix(event.Name, "assets") && event.Op&(fsnotify.Create) != 0 { + time.Sleep(300 * time.Millisecond) // Allow some time for the file system to settle eventChan <- event.Name } case err := <-watcher.Errors: @@ -1366,87 +1325,33 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } }() - // Start a goroutine to process file change events in batches go func() { for { - // Wait for the first event from the channel path, ok := <-eventChan if !ok { return } - paths := []string{path} - - // Set a 2-second window to collect additional events - collectWindow := time.After(2 * time.Second) - - loop: - for { - select { - // If another file change event comes in before 2s, collect it - case p := <-eventChan: - paths = append(paths, p) - - // If no events arrive within the window, break and process collected paths - case <-collectWindow: - break loop - } - } - - affectedScreens := make(map[string]bool) - sharedChanged := false - - for _, p := range paths { - relPath, _ := filepath.Rel(distAssetsPath, p) - relPath = filepath.ToSlash(relPath) - - switch { - case strings.HasPrefix(relPath, "shared/"), strings.HasPrefix(relPath, "main-") && strings.HasSuffix(relPath, ".js"): - sharedChanged = true - break - - default: - parts := strings.Split(relPath, "/") - if len(parts) >= 2 { - screen := parts[0] - if screenSet[screen] { - affectedScreens[screen] = true - } else { - log.Printf("ℹ️ Ignored change from unknown or untracked folder: %q", screen) - } - } - } - } + time.Sleep(300 * time.Millisecond) // short delay to let writes settle + log.Printf("🔁 Detected change in: %s — patching screens...", path) var wg sync.WaitGroup errChan := make(chan error, len(screensToWatch)) - if sharedChanged { - for _, screen := range screensToWatch { - wg.Add(1) - go func(screen string) { - defer wg.Done() - if err = patchScreen(ctx, cli, ScreenPromptMap[screen], assetsURL, distAssetsPath, screen); err != nil { - errChan <- fmt.Errorf("❌ Failed to patch screen %q: %v", screen, err) - } - }(screen) - } - } else { - for screen := range affectedScreens { - wg.Add(1) - go func(screen string) { - defer wg.Done() - if err = patchScreen(ctx, cli, ScreenPromptMap[screen], assetsURL, distAssetsPath, screen); err != nil { - errChan <- fmt.Errorf("❌ Failed to patch screen %q: %v", screen, err) - } - }(screen) - } + for _, screen := range screensToWatch { + wg.Add(1) + go func(screen string) { + defer wg.Done() + if err := patchScreen(ctx, cli, assetsURL, distAssetsPath, screen); err != nil { + errChan <- fmt.Errorf("❌ Failed to patch screen %q: %v", screen, err) + } + }(screen) } wg.Wait() close(errChan) - for err = range errChan { + for err := range errChan { log.Println(err) } } @@ -1456,8 +1361,18 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc return nil } -func isAssetFile(name string) bool { - return strings.HasSuffix(name, ".js") || strings.HasSuffix(name, ".css") +func patchScreen(ctx context.Context, cli *cli, assetsURL, distAssetsPath, screen string) error { + var settings management.PromptRendering + headTags, err := buildHeadTagsFromDirs(filepath.Dir(distAssetsPath), assetsURL, screen) + if err != nil { + return fmt.Errorf("failed to build head tags for %s: %w", screen, err) + } + settings.HeadTags = headTags + if err := updateSettings(ctx, cli, screen, &settings); err != nil { + return fmt.Errorf("failed to update settings for %s: %w", screen, err) + } + log.Printf("✅ Patched screen: %s", screen) + return nil } func buildHeadTagsFromDirs(distPath, assetsURL, screen string) ([]interface{}, error) { @@ -1473,23 +1388,34 @@ func buildHeadTagsFromDirs(distPath, assetsURL, screen string) ([]interface{}, e if err != nil { continue // skip on error } + for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() - src := fmt.Sprintf("%s/assets/%s/%s", assetsURL, filepath.Base(dir), name) + subDir := filepath.Base(dir) + if subDir == "assets" { + subDir = "" // root-level main-*.js + } + src := fmt.Sprintf("%s/assets/%s%s", assetsURL, subDir, name) + if subDir != "" { + src = fmt.Sprintf("%s/assets/%s/%s", assetsURL, subDir, name) + } + + ext := filepath.Ext(name) - switch { - case strings.HasSuffix(name, ".js"): + switch ext { + case ".js": tags = append(tags, map[string]interface{}{ "tag": "script", "attributes": map[string]interface{}{ "src": src, "defer": true, + "type": "module", }, }) - case strings.HasSuffix(name, ".css"): + case ".css": tags = append(tags, map[string]interface{}{ "tag": "link", "attributes": map[string]interface{}{ From 01b375b0dcc56bb37c4f6fb7c1ffc16aaf6e4efc Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Fri, 11 Jul 2025 08:42:46 +0530 Subject: [PATCH 07/58] final code to support watching screens --- internal/cli/universal_login_customize.go | 118 +++++++++++----------- 1 file changed, 61 insertions(+), 57 deletions(-) diff --git a/internal/cli/universal_login_customize.go b/internal/cli/universal_login_customize.go index 7f4126b09..ddb9364b5 100644 --- a/internal/cli/universal_login_customize.go +++ b/internal/cli/universal_login_customize.go @@ -1304,75 +1304,79 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc log.Printf("👀 Watching: %d screen(s): %v", len(screensToWatch), screensToWatch) } - eventChan := make(chan string, 100) + const debounceWindow = 5 * time.Second + var lastProcessTime time.Time + lastHeadTags := make(map[string][]interface{}) - go func() { - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return - } - if strings.HasSuffix(event.Name, "assets") && event.Op&(fsnotify.Create) != 0 { - time.Sleep(300 * time.Millisecond) // Allow some time for the file system to settle - eventChan <- event.Name - } - case err := <-watcher.Errors: - log.Printf("❗ Watcher error: %v", err) - case <-ctx.Done(): - return - } - } - }() - - go func() { - for { - path, ok := <-eventChan + for { + select { + case event, ok := <-watcher.Events: if !ok { - return + return nil } - time.Sleep(300 * time.Millisecond) // short delay to let writes settle - log.Printf("🔁 Detected change in: %s — patching screens...", path) + if strings.HasSuffix(event.Name, "assets") && event.Op&(fsnotify.Create) != 0 { + now := time.Now() + if now.Sub(lastProcessTime) < debounceWindow { + log.Println("⏱️ Ignoring event due to debounce window") + continue + } + lastProcessTime = now - var wg sync.WaitGroup - errChan := make(chan error, len(screensToWatch)) + time.Sleep(500 * time.Millisecond) // short delay to let writes settle + log.Println("📦 Change detected in assets folder. Rebuilding and patching...") - for _, screen := range screensToWatch { - wg.Add(1) - go func(screen string) { - defer wg.Done() - if err := patchScreen(ctx, cli, assetsURL, distAssetsPath, screen); err != nil { - errChan <- fmt.Errorf("❌ Failed to patch screen %q: %v", screen, err) - } - }(screen) - } + var wg sync.WaitGroup + errChan := make(chan error, len(screensToWatch)) + + for _, screen := range screensToWatch { + wg.Add(1) + + go func(screen string) { + defer wg.Done() + + headTags, err := buildHeadTagsFromDirs(filepath.Dir(distAssetsPath), assetsURL, screen) + if err != nil { + errChan <- fmt.Errorf("failed to build headTags for %s: %w", screen, err) + return + } - wg.Wait() - close(errChan) + if reflect.DeepEqual(lastHeadTags[screen], headTags) { + log.Printf("🔁 Skipping patch for '%s' — headTags unchanged", screen) + return + } - for err := range errChan { - log.Println(err) + log.Printf("📦 Detected changes for screen '%s'", screen) + lastHeadTags[screen] = headTags + + var settings = &management.PromptRendering{ + HeadTags: headTags, + } + + if err = cli.api.Prompt.UpdateRendering(ctx, management.PromptType(ScreenPromptMap[screen]), management.ScreenName(screen), settings); err != nil { + errChan <- fmt.Errorf("failed to patch settings for %s: %w", screen, err) + return + } + + log.Printf("✅ Successfully patched screen '%s'", screen) + }(screen) + } + + wg.Wait() + close(errChan) + + for err = range errChan { + log.Println(err) + } } - } - }() - <-ctx.Done() - return nil -} + case err = <-watcher.Errors: + log.Println("Watcher error: ", err) -func patchScreen(ctx context.Context, cli *cli, assetsURL, distAssetsPath, screen string) error { - var settings management.PromptRendering - headTags, err := buildHeadTagsFromDirs(filepath.Dir(distAssetsPath), assetsURL, screen) - if err != nil { - return fmt.Errorf("failed to build head tags for %s: %w", screen, err) - } - settings.HeadTags = headTags - if err := updateSettings(ctx, cli, screen, &settings); err != nil { - return fmt.Errorf("failed to update settings for %s: %w", screen, err) + case <-ctx.Done(): + return ctx.Err() + } } - log.Printf("✅ Patched screen: %s", screen) - return nil } func buildHeadTagsFromDirs(distPath, assetsURL, screen string) ([]interface{}, error) { From 462fa80dc4bca28933a307e99da047609f13a2af Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Fri, 11 Jul 2025 13:30:27 +0530 Subject: [PATCH 08/58] Update docs and examples --- docs/auth0_universal-login.md | 1 + docs/auth0_universal-login_customize.md | 1 + docs/auth0_universal-login_show.md | 1 + docs/auth0_universal-login_switch.md | 1 + docs/auth0_universal-login_update.md | 1 + internal/cli/universal_login_customize.go | 55 ++++++++++++----------- 6 files changed, 34 insertions(+), 26 deletions(-) diff --git a/docs/auth0_universal-login.md b/docs/auth0_universal-login.md index 9440e021e..142eff257 100644 --- a/docs/auth0_universal-login.md +++ b/docs/auth0_universal-login.md @@ -15,4 +15,5 @@ Manage a consistent, branded Universal Login experience that can handle all of y - [auth0 universal-login switch](auth0_universal-login_switch.md) - Switch the rendering mode for Universal Login - [auth0 universal-login templates](auth0_universal-login_templates.md) - Manage custom Universal Login templates - [auth0 universal-login update](auth0_universal-login_update.md) - Update the custom branding settings for Universal Login +- [auth0 universal-login watch-assets](auth0_universal-login_watch-assets.md) - Watch dist folder and patch screen assets. We can watch for all or 1 or more screens. diff --git a/docs/auth0_universal-login_customize.md b/docs/auth0_universal-login_customize.md index 61c8f4995..bc473fc03 100644 --- a/docs/auth0_universal-login_customize.md +++ b/docs/auth0_universal-login_customize.md @@ -67,5 +67,6 @@ auth0 universal-login customize [flags] - [auth0 universal-login switch](auth0_universal-login_switch.md) - Switch the rendering mode for Universal Login - [auth0 universal-login templates](auth0_universal-login_templates.md) - Manage custom Universal Login templates - [auth0 universal-login update](auth0_universal-login_update.md) - Update the custom branding settings for Universal Login +- [auth0 universal-login watch-assets](auth0_universal-login_watch-assets.md) - Watch dist folder and patch screen assets. We can watch for all or 1 or more screens. diff --git a/docs/auth0_universal-login_show.md b/docs/auth0_universal-login_show.md index 46ce817db..7406b308e 100644 --- a/docs/auth0_universal-login_show.md +++ b/docs/auth0_universal-login_show.md @@ -46,5 +46,6 @@ auth0 universal-login show [flags] - [auth0 universal-login switch](auth0_universal-login_switch.md) - Switch the rendering mode for Universal Login - [auth0 universal-login templates](auth0_universal-login_templates.md) - Manage custom Universal Login templates - [auth0 universal-login update](auth0_universal-login_update.md) - Update the custom branding settings for Universal Login +- [auth0 universal-login watch-assets](auth0_universal-login_watch-assets.md) - Watch dist folder and patch screen assets. We can watch for all or 1 or more screens. diff --git a/docs/auth0_universal-login_switch.md b/docs/auth0_universal-login_switch.md index ed44db77e..ec812ffaa 100644 --- a/docs/auth0_universal-login_switch.md +++ b/docs/auth0_universal-login_switch.md @@ -51,5 +51,6 @@ auth0 universal-login switch [flags] - [auth0 universal-login switch](auth0_universal-login_switch.md) - Switch the rendering mode for Universal Login - [auth0 universal-login templates](auth0_universal-login_templates.md) - Manage custom Universal Login templates - [auth0 universal-login update](auth0_universal-login_update.md) - Update the custom branding settings for Universal Login +- [auth0 universal-login watch-assets](auth0_universal-login_watch-assets.md) - Watch dist folder and patch screen assets. We can watch for all or 1 or more screens. diff --git a/docs/auth0_universal-login_update.md b/docs/auth0_universal-login_update.md index 75713061f..2e49c083f 100644 --- a/docs/auth0_universal-login_update.md +++ b/docs/auth0_universal-login_update.md @@ -56,5 +56,6 @@ auth0 universal-login update [flags] - [auth0 universal-login switch](auth0_universal-login_switch.md) - Switch the rendering mode for Universal Login - [auth0 universal-login templates](auth0_universal-login_templates.md) - Manage custom Universal Login templates - [auth0 universal-login update](auth0_universal-login_update.md) - Update the custom branding settings for Universal Login +- [auth0 universal-login watch-assets](auth0_universal-login_watch-assets.md) - Watch dist folder and patch screen assets. We can watch for all or 1 or more screens. diff --git a/internal/cli/universal_login_customize.go b/internal/cli/universal_login_customize.go index ddb9364b5..f1c5b8545 100644 --- a/internal/cli/universal_login_customize.go +++ b/internal/cli/universal_login_customize.go @@ -99,12 +99,28 @@ var ( IsRequired: true, } + watchFolder = Flag{ + Name: "Watch Folder", + LongForm: "watch-folder", + ShortForm: "w", + Help: "Folder to watch for new builds. CLI will watch for changes in the folder and automatically update the assets.", + IsRequired: true, + } + + assetURL = Flag{ + Name: "Assets URL", + LongForm: "assets-url", + ShortForm: "u", + Help: "Base URL for serving dist assets (e.g., http://localhost:5173).", + IsRequired: true, + } + screens1 = Flag{ Name: "screen", LongForm: "screens", ShortForm: "s", Help: "watching screens", - IsRequired: false, + IsRequired: true, AlwaysPrompt: true, } ) @@ -1226,41 +1242,28 @@ func switchUniversalLoginRendererModeCmd(cli *cli) *cobra.Command { return cmd } func newUpdateAssetsCmd(cli *cli) *cobra.Command { - var watchFolder, assetURL string + var watchFolders, assetsURL string var screens []string cmd := &cobra.Command{ Use: "watch-assets", - Short: "Watch dist folder and patch screen assets", + Short: "Watch the dist folder and patch screen assets. You can watch all screens or one or more specific screens.", + Example: ` auth0 universal-login watch-assets --screens login-id,login,signup,email-identifier-challenge,login-passwordless-email-code --watch-folder "/dist" --assets-url "http://localhost:8080" + auth0 ul watch-assets --screens all -w "/dist" -u "http://localhost:8080" + auth0 ul watch-assets --screen login-id --watch-folder "/dist"" --assets-url "http://localhost:8080" + auth0 ul switch -p login-id -s login-id -r standard`, RunE: func(cmd *cobra.Command, args []string) error { - return watchAndPatch(context.Background(), cli, assetURL, watchFolder, screens) + return watchAndPatch(context.Background(), cli, assetsURL, watchFolders, screens) }, } screens1.RegisterStringSlice(cmd, &screens, nil) - cmd.Flags().StringVar(&watchFolder, "watch-folder", "", "Folder to watch for new builds") - cmd.Flags().StringVar(&assetURL, "assets-url", "", "Base URL for serving dist assets (e.g., http://localhost:5173)") - cmd.MarkFlagRequired("screens1") - cmd.MarkFlagRequired("watch-folder") - cmd.MarkFlagRequired("asset-url") + watchFolder.RegisterString(cmd, &watchFolders, "") + assetURL.RegisterString(cmd, &assetsURL, "") return cmd } -func updateSettings(ctx context.Context, cli *cli, screenName string, settings *management.PromptRendering) error { - if settings == nil { - return fmt.Errorf("settings or rendering mode is nil") - } - - if err := ansi.Waiting(func() error { - return cli.api.Prompt.UpdateRendering(ctx, management.PromptType(ScreenPromptMap[screenName]), management.ScreenName(screenName), settings) - }); err != nil { - return fmt.Errorf("failed to set the render settings: %w", err) - } - - return nil -} - func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screenDirs []string) error { watcher, err := fsnotify.NewWatcher() if err != nil { @@ -1287,11 +1290,11 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc path := filepath.Join(distAssetsPath, screen) info, err := os.Stat(path) if err != nil { - log.Printf("⚠️ screen directory %q not found in dist/assets: %v", screen, err) + log.Printf("Screen directory %q not found in dist/assets: %v", screen, err) continue } if !info.IsDir() { - log.Printf("⚠️ screen path %q exists but is not a directory", path) + log.Printf("Screen path %q exists but is not a directory", path) continue } screensToWatch = append(screensToWatch, screen) @@ -1299,7 +1302,7 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } if err := watcher.Add(distPath); err != nil { - log.Printf("⚠️ Failed to watch %q: %v", distPath, err) + log.Printf("Failed to watch %q: %v", distPath, err) } else { log.Printf("👀 Watching: %d screen(s): %v", len(screensToWatch), screensToWatch) } From c3397dd7ca89dd0addd48412e502ef6a5bad0ef5 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 14 Jul 2025 22:41:53 +0530 Subject: [PATCH 09/58] Add docs for watch and patch assets --- docs/auth0_universal-login_watch-assets.md | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/auth0_universal-login_watch-assets.md diff --git a/docs/auth0_universal-login_watch-assets.md b/docs/auth0_universal-login_watch-assets.md new file mode 100644 index 000000000..3c155319e --- /dev/null +++ b/docs/auth0_universal-login_watch-assets.md @@ -0,0 +1,54 @@ +--- +layout: default +parent: auth0 universal-login +has_toc: false +--- +# auth0 universal-login watch-assets + + + +## Usage +``` +auth0 universal-login watch-assets [flags] +``` + +## Examples + +``` + auth0 universal-login watch-assets --screens login-id,login,signup,email-identifier-challenge,login-passwordless-email-code --watch-folder "/dist" --assets-url "http://localhost:8080" + auth0 ul watch-assets --screens all -w "/dist" -u "http://localhost:8080" + auth0 ul watch-assets --screen login-id --watch-folder "/dist"" --assets-url "http://localhost:8080" + auth0 ul switch -p login-id -s login-id -r standard +``` + + +## Flags + +``` + -u, --assets-url string Base URL for serving dist assets (e.g., http://localhost:5173). + -s, --screens strings watching screens + -w, --watch-folder string Folder to watch for new builds. CLI will watch for changes in the folder and automatically update the assets. +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 universal-login customize](auth0_universal-login_customize.md) - Customize the Universal Login experience for the standard or advanced mode +- [auth0 universal-login prompts](auth0_universal-login_prompts.md) - Manage custom text for prompts +- [auth0 universal-login show](auth0_universal-login_show.md) - Display the custom branding settings for Universal Login +- [auth0 universal-login switch](auth0_universal-login_switch.md) - Switch the rendering mode for Universal Login +- [auth0 universal-login templates](auth0_universal-login_templates.md) - Manage custom Universal Login templates +- [auth0 universal-login update](auth0_universal-login_update.md) - Update the custom branding settings for Universal Login +- [auth0 universal-login watch-assets](auth0_universal-login_watch-assets.md) - Watch dist folder and patch screen assets. We can watch for all or 1 or more screens. + + From 8ce28b9b465e2815f62be4615970874adde36b71 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Wed, 27 Aug 2025 12:53:12 +0530 Subject: [PATCH 10/58] Add scaffolding commands(POC) and documentation for acul app initialization --- internal/cli/acul.go | 3 + internal/cli/acul_sacffolding_app.MD | 53 ++++++ internal/cli/acul_sca.go | 248 +++++++++++++++++++++++++++ internal/cli/acul_scaff.go | 138 +++++++++++++++ internal/cli/acul_scaffolding.go | 218 +++++++++++++++++++++++ 5 files changed, 660 insertions(+) create mode 100644 internal/cli/acul_sacffolding_app.MD create mode 100644 internal/cli/acul_sca.go create mode 100644 internal/cli/acul_scaff.go create mode 100644 internal/cli/acul_scaffolding.go diff --git a/internal/cli/acul.go b/internal/cli/acul.go index 64f087dbd..278186b39 100644 --- a/internal/cli/acul.go +++ b/internal/cli/acul.go @@ -180,6 +180,9 @@ func aculCmd(cli *cli) *cobra.Command { } cmd.AddCommand(aculConfigureCmd(cli)) + cmd.AddCommand(aculInitCmd(cli)) + cmd.AddCommand(aculInitCmd1(cli)) + cmd.AddCommand(aculInitCmd2(cli)) return cmd } diff --git a/internal/cli/acul_sacffolding_app.MD b/internal/cli/acul_sacffolding_app.MD new file mode 100644 index 000000000..7089e66f6 --- /dev/null +++ b/internal/cli/acul_sacffolding_app.MD @@ -0,0 +1,53 @@ +# Scaffolding Approaches: Comparison and Trade-offs + +## Method A: Git Sparse-Checkout +*File: `internal/cli/acul_scaff.go` (command `init1`)* + +**Summary:** +Initializes a git repo in the target directory, enables sparse-checkout, writes desired paths, and pulls from branch `monorepo-sample`. + +**Pros:** +- Efficient for large repos; downloads only needed paths. +- Preserves git-tracked file modes and line endings. +- Simple incremental updates (pull/merge) are possible. +- Works with private repos once user’s git is authenticated. + +**Cons:** +- Requires `git` installed and a relatively recent version for sparse-checkout. + + +--- + +## Method B: HTTP Raw + GitHub Tree API +*File: `internal/cli/acul_scaffolding.go` (command `init`)* + +**Summary:** +Uses the GitHub Tree API to enumerate files and `raw.githubusercontent.com` to download each file individually to a target folder. + +**Pros:** +- No git dependency; pure HTTP. +- Fine-grained control over exactly which files to fetch. +- Easier sandboxing; fewer environment assumptions. + +**Cons:** +- Many HTTP requests; slower and susceptible to GitHub API rate limits. +- Loses executable bits and some metadata unless explicitly restored. + + +--- + +## Method C: Zip Download + Selective Copy +*File: `internal/cli/acul_sca.go` (command `init2`)* + +**Summary:** +Downloads a branch zip archive once, unzips to a temp directory, then copies only base directories/files and selected screens into the target directory. + +**Pros:** +- Single network transfer; fast and API-rate-limit friendly. +- No git dependency; works in minimal environments. +- Simple to reason about and easy to clean up. +- Good for reproducible scaffolds at a specific ref (if pinned). + +**Cons:** +- Requires extra disk for the zip and the unzipped tree. +- Tightly coupled to the zip’s top-level folder name prefix. \ No newline at end of file diff --git a/internal/cli/acul_sca.go b/internal/cli/acul_sca.go new file mode 100644 index 000000000..4a3582903 --- /dev/null +++ b/internal/cli/acul_sca.go @@ -0,0 +1,248 @@ +package cli + +import ( + "fmt" + "github.com/auth0/auth0-cli/internal/utils" + "github.com/spf13/cobra" + "io" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/auth0/auth0-cli/internal/prompt" +) + +// This logic goes inside your `RunE` function. +func aculInitCmd2(c *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "init2", + Args: cobra.MaximumNArgs(1), + Short: "Generate a new project from a template", + Long: `Generate a new project from a template.`, + RunE: runScaffold2, + } + + return cmd + +} + +func runScaffold2(cmd *cobra.Command, args []string) error { + // Step 1: fetch manifest.json + manifest, err := fetchManifest() + if err != nil { + return err + } + + // Step 2: select template + var templateNames []string + for k := range manifest.Templates { + templateNames = append(templateNames, k) + } + + var chosen string + promptText := prompt.SelectInput("", "Select a template", "Chosen template(Todo)", utils.FetchKeys(manifest.Templates), "react-js", true) + if err := prompt.AskOne(promptText, &chosen); err != nil { + fmt.Println(err) + } + + // Step 3: select screens + var screenOptions []string + template := manifest.Templates[chosen] + for _, s := range template.Screens { + screenOptions = append(screenOptions, s.ID) + } + + // Step 3: Let user select screens + var selectedScreens []string + if err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...); err != nil { + return err + } + + // Step 3: Create project folder + var destDir string + if len(args) < 1 { + destDir = "my_acul_proj2" + } else { + destDir = args[0] + } + if err := os.MkdirAll(destDir, 0755); err != nil { + return fmt.Errorf("failed to create project dir: %w", err) + } + + curr := time.Now() + + // --- Step 1: Download and Unzip to Temp Dir --- + repoURL := "https://github.com/auth0-samples/auth0-acul-samples/archive/refs/heads/monorepo-sample.zip" + tempZipFile := downloadFile(repoURL) + defer os.Remove(tempZipFile) // Clean up the temp zip file + + tempUnzipDir, err := os.MkdirTemp("", "unzipped-repo-*") + check(err, "Error creating temporary unzipped directory") + defer os.RemoveAll(tempUnzipDir) // Clean up the entire temp directory + + err = utils.Unzip(tempZipFile, tempUnzipDir) + if err != nil { + return err + } + + // TODO: Adjust this prefix based on the actual structure of the unzipped content(once main branch is used) + const sourcePathPrefix = "auth0-acul-samples-monorepo-sample/" + + // --- Step 2: Copy the Specified Base Directories --- + for _, dir := range manifest.Templates[chosen].BaseDirectories { + srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, dir) + destPath := filepath.Join(destDir, dir) + + if _, err := os.Stat(srcPath); os.IsNotExist(err) { + log.Printf("Warning: Source directory does not exist: %s", srcPath) + continue + } + + fmt.Printf("Copying directory: %s\n", dir) + err := copyDir(srcPath, destPath) + check(err, fmt.Sprintf("Error copying directory %s", dir)) + } + + // --- Step 3: Copy the Specified Base Files --- + for _, baseFile := range manifest.Templates[chosen].BaseFiles { + srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, baseFile) + destPath := filepath.Join(destDir, baseFile) + + if _, err = os.Stat(srcPath); os.IsNotExist(err) { + log.Printf("Warning: Source file does not exist: %s", srcPath) + continue + } + + //parentDir := filepath.Dir(destPath) + //if err := os.MkdirAll(parentDir, 0755); err != nil { + // log.Printf("Error creating parent directory for %s: %v", baseFile, err) + // continue + //} + + fmt.Printf("Copying file: %s\n", baseFile) + err := copyFile(srcPath, destPath) + check(err, fmt.Sprintf("Error copying file %s", baseFile)) + } + + screenInfo := createScreenMap(template.Screens) + for _, s := range selectedScreens { + screen := screenInfo[s] + + srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, screen.Path) + destPath := filepath.Join(destDir, screen.Path) + + if _, err = os.Stat(srcPath); os.IsNotExist(err) { + log.Printf("Warning: Source directory does not exist: %s", srcPath) + continue + } + + //parentDir := filepath.Dir(destPath) + //if err := os.MkdirAll(parentDir, 0755); err != nil { + // log.Printf("Error creating parent directory for %s: %v", screen.Path, err) + // continue + //} + + fmt.Printf("Copying screen file: %s\n", screen.Path) + err := copyFile(srcPath, destPath) + check(err, fmt.Sprintf("Error copying screen file %s", screen.Path)) + + } + + fmt.Println("\nSuccess! The files and directories have been copied.") + + fmt.Println(time.Since(curr)) + + return nil +} + +// Helper function to handle errors and log them +func check(err error, msg string) { + if err != nil { + log.Fatalf("%s: %v", err, msg) + } +} + +// Function to download a file from a URL to a temporary location +func downloadFile(url string) string { + tempFile, err := os.CreateTemp("", "github-zip-*.zip") + check(err, "Error creating temporary file") + + fmt.Printf("Downloading from %s...\n", url) + resp, err := http.Get(url) + check(err, "Error downloading file") + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Fatalf("Bad status code: %s", resp.Status) + } + + _, err = io.Copy(tempFile, resp.Body) + check(err, "Error saving zip file") + tempFile.Close() + + return tempFile.Name() +} + +// Function to copy a file from a source path to a destination path +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return fmt.Errorf("failed to open source file: %w", err) + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return fmt.Errorf("failed to create destination file: %w", err) + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return fmt.Errorf("failed to copy file contents: %w", err) + } + return out.Close() +} + +// Function to recursively copy a directory +func copyDir(src, dst string) error { + sourceInfo, err := os.Stat(src) + if err != nil { + return err + } + + err = os.MkdirAll(dst, sourceInfo.Mode()) + if err != nil { + return err + } + + return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if path == src { + return nil + } + + relPath, err := filepath.Rel(src, path) + if err != nil { + return err + } + destPath := filepath.Join(dst, relPath) + + if info.IsDir() { + return os.MkdirAll(destPath, info.Mode()) + } + return copyFile(path, destPath) + }) +} + +func createScreenMap(screens []Screen) map[string]Screen { + screenMap := make(map[string]Screen) + for _, screen := range screens { + screenMap[screen.Name] = screen + } + return screenMap +} diff --git a/internal/cli/acul_scaff.go b/internal/cli/acul_scaff.go new file mode 100644 index 000000000..d501b881b --- /dev/null +++ b/internal/cli/acul_scaff.go @@ -0,0 +1,138 @@ +package cli + +import ( + "fmt" + "github.com/auth0/auth0-cli/internal/utils" + "github.com/spf13/cobra" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/auth0/auth0-cli/internal/prompt" +) + +// This logic goes inside your `RunE` function. +func aculInitCmd1(c *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "init1", + Args: cobra.MaximumNArgs(1), + Short: "Generate a new project from a template", + Long: `Generate a new project from a template.`, + RunE: runScaffold, + } + + return cmd + +} + +func runScaffold(cmd *cobra.Command, args []string) error { + + // Step 1: fetch manifest.json + manifest, err := fetchManifest() + if err != nil { + return err + } + + // Step 2: select template + templateNames := []string{} + for k := range manifest.Templates { + templateNames = append(templateNames, k) + } + + var chosen string + promptText := prompt.SelectInput("", "Select a template", "Chosen template(Todo)", utils.FetchKeys(manifest.Templates), "react-js", true) + if err := prompt.AskOne(promptText, &chosen); err != nil { + + } + + // Step 3: select screens + var screenOptions []string + template := manifest.Templates[chosen] + for _, s := range template.Screens { + screenOptions = append(screenOptions, s.ID) + } + + // Step 3: Let user select screens + var selectedScreens []string + if err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...); err != nil { + return err + } + + // Step 3: Create project folder + var projectDir string + if len(args) < 1 { + projectDir = "my_acul_proj1" + } else { + projectDir = args[0] + } + if err := os.MkdirAll(projectDir, 0755); err != nil { + return fmt.Errorf("failed to create project dir: %w", err) + } + + curr := time.Now() + + // Step 4: Init git repo + + repoURL := "https://github.com/auth0-samples/auth0-acul-samples.git" + if err := runGit(projectDir, "init"); err != nil { + return err + } + if err := runGit(projectDir, "remote", "add", "-f", "origin", repoURL); err != nil { + return err + } + if err := runGit(projectDir, "config", "core.sparseCheckout", "true"); err != nil { + return err + } + + // Step 5: Write sparse-checkout paths + baseFiles := manifest.Templates[chosen].BaseFiles + baseDirectories := manifest.Templates[chosen].BaseDirectories + + paths := append(baseFiles, baseDirectories...) + paths = append(paths, selectedScreens...) + + for _, scr := range template.Screens { + for _, chosenScreen := range selectedScreens { + if scr.Name == chosenScreen { + paths = append(paths, scr.Path) + } + } + } + + sparseFile := filepath.Join(projectDir, ".git", "info", "sparse-checkout") + + f, err := os.Create(sparseFile) + if err != nil { + return fmt.Errorf("failed to write sparse-checkout file: %w", err) + } + + for _, p := range paths { + _, _ = f.WriteString(p + "\n") + } + + f.Close() + + // Step 6: Pull only sparse files + if err := runGit(projectDir, "pull", "origin", "monorepo-sample"); err != nil { + return err + } + + // Step 7: Clean up .git + //if err := os.RemoveAll(filepath.Join(projectDir, ".git")); err != nil { + // return fmt.Errorf("failed to clean up git metadata: %w", err) + //} + + fmt.Println(time.Since(curr)) + + fmt.Printf("✅ Project scaffolded successfully in %s\n", projectDir) + return nil +} + +func runGit(dir string, args ...string) error { + cmd := exec.Command("git", args...) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/internal/cli/acul_scaffolding.go b/internal/cli/acul_scaffolding.go new file mode 100644 index 000000000..4ce681140 --- /dev/null +++ b/internal/cli/acul_scaffolding.go @@ -0,0 +1,218 @@ +package cli + +import ( + "encoding/json" + "fmt" + "github.com/auth0/auth0-cli/internal/prompt" + "github.com/auth0/auth0-cli/internal/utils" + "github.com/spf13/cobra" + "io" + "net/http" + "os" + "path/filepath" + "time" +) + +type Manifest struct { + Templates map[string]Template `json:"templates"` + Metadata Metadata `json:"metadata"` +} + +type Template struct { + Name string `json:"name"` + Description string `json:"description"` + Framework string `json:"framework"` + SDK string `json:"sdk"` + BaseFiles []string `json:"base_files"` + BaseDirectories []string `json:"base_directories"` + Screens []Screen `json:"screens"` +} + +type Screen struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Path string `json:"path"` +} + +type Metadata struct { + Version string `json:"version"` + Repository string `json:"repository"` + LastUpdated string `json:"last_updated"` + Description string `json:"description"` +} + +// raw GitHub base URL +const rawBaseURL = "https://raw.githubusercontent.com" + +func main() { + +} + +func fetchManifest() (*Manifest, error) { + // The URL to the raw JSON file in the repository. + url := "https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/monorepo-sample/manifest.json" + + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("cannot fetch manifest: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch manifest: received status code %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("cannot read manifest body: %w", err) + } + + var manifest Manifest + if err := json.Unmarshal(body, &manifest); err != nil { + return nil, fmt.Errorf("invalid manifest format: %w", err) + } + + return &manifest, nil +} + +// This logic goes inside your `RunE` function. +func aculInitCmd(c *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "init", + Args: cobra.MaximumNArgs(1), + Short: "Generate a new project from a template", + Long: `Generate a new project from a template.`, + RunE: func(cmd *cobra.Command, args []string) error { + + manifest, err := fetchManifest() + if err != nil { + return err + } + + // Step 2: select template + var templateNames []string + for k := range manifest.Templates { + templateNames = append(templateNames, k) + } + + var chosen string + promptText := prompt.SelectInput("", "Select a template", "Chosen template(Todo)", utils.FetchKeys(manifest.Templates), "react-js", true) + if err := prompt.AskOne(promptText, &chosen); err != nil { + + } + + // Step 3: select screens + var screenOptions []string + template := manifest.Templates[chosen] + for _, s := range template.Screens { + screenOptions = append(screenOptions, s.ID) + } + + // Step 3: Let user select screens + var selectedScreens []string + if err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...); err != nil { + return err + } + + var targetRoot string + if len(args) < 1 { + targetRoot = "my_acul_proj" + } else { + targetRoot = args[0] + } + + if err := os.MkdirAll(targetRoot, 0755); err != nil { + return fmt.Errorf("failed to create project dir: %w", err) + } + + curr := time.Now() + + fmt.Println(time.Since(curr)) + + fmt.Println("✅ Scaffolding complete") + + return nil + }, + } + + return cmd + +} + +const baseRawURL = "https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/monorepo-sample" + +// GitHub API base for directory traversal +const baseTreeAPI = "https://api.github.com/repos/auth0-samples/auth0-acul-samples/git/trees/monorepo-sample?recursive=1" + +// downloadRaw fetches a single file and saves it locally. +func downloadRaw(path, destDir string) error { + url := fmt.Sprintf("%s/%s", baseRawURL, path) + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("failed to fetch %s: %w", url, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to fetch %s: %s", url, resp.Status) + } + + // Create destination path + destPath := filepath.Join(destDir, path) + if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { + return fmt.Errorf("failed to create dirs for %s: %w", destPath, err) + } + + // Write file + out, err := os.Create(destPath) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", destPath, err) + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + if err != nil { + return fmt.Errorf("failed to write %s: %w", destPath, err) + } + + return nil +} + +// GitHub tree API response +type treeEntry struct { + Path string `json:"path"` + Type string `json:"type"` // "blob" (file) or "tree" (dir) + URL string `json:"url"` +} + +type treeResponse struct { + Tree []treeEntry `json:"tree"` +} + +// downloadDirectory downloads all files under a given directory using GitHub Tree API. +func downloadDirectory(dir, destDir string) error { + resp, err := http.Get(baseTreeAPI) + if err != nil { + return fmt.Errorf("failed to fetch tree: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to fetch tree API: %s", resp.Status) + } + + var tr treeResponse + if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil { + return fmt.Errorf("failed to decode tree: %w", err) + } + + for _, entry := range tr.Tree { + if entry.Type == "blob" && filepath.HasPrefix(entry.Path, dir) { + if err := downloadRaw(entry.Path, destDir); err != nil { + return err + } + } + } + return nil +} From 8b4d176297073279e28daf6cb4dd39f7a65384fa Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Sun, 7 Sep 2025 15:50:14 +0530 Subject: [PATCH 11/58] update docs and fix lints --- docs/auth0_acul.md | 2 + docs/auth0_acul_config.md | 2 + docs/auth0_acul_init1.md | 40 ++ docs/auth0_acul_init2.md | 40 ++ internal/cli/acul.go | 565 +-------------------------- internal/cli/acul_config.go | 564 ++++++++++++++++++++++++++ internal/cli/acul_sacffolding_app.MD | 1 + internal/cli/acul_sca.go | 137 ++++--- internal/cli/acul_scaff.go | 109 ++++-- internal/cli/acul_scaffolding.go | 218 ----------- 10 files changed, 809 insertions(+), 869 deletions(-) create mode 100644 docs/auth0_acul_init1.md create mode 100644 docs/auth0_acul_init2.md create mode 100644 internal/cli/acul_config.go delete mode 100644 internal/cli/acul_scaffolding.go diff --git a/docs/auth0_acul.md b/docs/auth0_acul.md index f2d4583f5..cda4317d2 100644 --- a/docs/auth0_acul.md +++ b/docs/auth0_acul.md @@ -10,4 +10,6 @@ Customize the Universal Login experience. This requires a custom domain to be co ## Commands - [auth0 acul config](auth0_acul_config.md) - Configure the Universal Login experience +- [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template +- [auth0 acul init2](auth0_acul_init2.md) - Generate a new project from a template diff --git a/docs/auth0_acul_config.md b/docs/auth0_acul_config.md index 636a045e2..1fbe86ae7 100644 --- a/docs/auth0_acul_config.md +++ b/docs/auth0_acul_config.md @@ -36,5 +36,7 @@ auth0 acul config [flags] ## Related Commands - [auth0 acul config](auth0_acul_config.md) - Configure the Universal Login experience +- [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template +- [auth0 acul init2](auth0_acul_init2.md) - Generate a new project from a template diff --git a/docs/auth0_acul_init1.md b/docs/auth0_acul_init1.md new file mode 100644 index 000000000..4beb1d249 --- /dev/null +++ b/docs/auth0_acul_init1.md @@ -0,0 +1,40 @@ +--- +layout: default +parent: auth0 acul +has_toc: false +--- +# auth0 acul init1 + +Generate a new project from a template. + +## Usage +``` +auth0 acul init1 [flags] +``` + +## Examples + +``` + +``` + + + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 acul config](auth0_acul_config.md) - Configure the Universal Login experience +- [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template +- [auth0 acul init2](auth0_acul_init2.md) - Generate a new project from a template + + diff --git a/docs/auth0_acul_init2.md b/docs/auth0_acul_init2.md new file mode 100644 index 000000000..5888f11b8 --- /dev/null +++ b/docs/auth0_acul_init2.md @@ -0,0 +1,40 @@ +--- +layout: default +parent: auth0 acul +has_toc: false +--- +# auth0 acul init2 + +Generate a new project from a template. + +## Usage +``` +auth0 acul init2 [flags] +``` + +## Examples + +``` + +``` + + + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 acul config](auth0_acul_config.md) - Configure the Universal Login experience +- [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template +- [auth0 acul init2](auth0_acul_init2.md) - Generate a new project from a template + + diff --git a/internal/cli/acul.go b/internal/cli/acul.go index 278186b39..c91902974 100644 --- a/internal/cli/acul.go +++ b/internal/cli/acul.go @@ -1,176 +1,6 @@ package cli -import ( - "encoding/json" - "errors" - "fmt" - "os" - "reflect" - "strconv" - - "github.com/pkg/browser" - - "github.com/auth0/go-auth0/management" - "github.com/spf13/cobra" - - "github.com/auth0/auth0-cli/internal/ansi" - "github.com/auth0/auth0-cli/internal/prompt" - "github.com/auth0/auth0-cli/internal/utils" -) - -var ( - screenName = Flag{ - Name: "Screen Name", - LongForm: "screen", - ShortForm: "s", - Help: "Name of the screen to customize.", - IsRequired: true, - } - file = Flag{ - Name: "File", - LongForm: "settings-file", - ShortForm: "f", - Help: "File to save the rendering configs to.", - IsRequired: false, - } - rendererScript = Flag{ - Name: "Script", - LongForm: "script", - ShortForm: "s", - Help: "Script contents for the rendering configs.", - IsRequired: true, - } - fieldsFlag = Flag{ - Name: "Fields", - LongForm: "fields", - Help: "Comma-separated list of fields to include or exclude in the result (based on value provided for include_fields) ", - IsRequired: false, - } - includeFieldsFlag = Flag{ - Name: "Include Fields", - LongForm: "include-fields", - Help: "Whether specified fields are to be included (default: true) or excluded (false).", - IsRequired: false, - } - includeTotalsFlag = Flag{ - Name: "Include Totals", - LongForm: "include-totals", - Help: "Return results inside an object that contains the total result count (true) or as a direct array of results (false).", - IsRequired: false, - } - pageFlag = Flag{ - Name: "Page", - LongForm: "page", - Help: "Page index of the results to return. First page is 0.", - IsRequired: false, - } - perPageFlag = Flag{ - Name: "Per Page", - LongForm: "per-page", - Help: "Number of results per page. Default value is 50, maximum value is 100.", - IsRequired: false, - } - promptFlag = Flag{ - Name: "Prompt", - LongForm: "prompt", - Help: "Filter by the Universal Login prompt.", - IsRequired: false, - } - screenFlag = Flag{ - Name: "Screen", - LongForm: "screen", - Help: "Filter by the Universal Login screen.", - IsRequired: false, - } - renderingModeFlag = Flag{ - Name: "Rendering Mode", - LongForm: "rendering-mode", - Help: "Filter by the rendering mode (advanced or standard).", - IsRequired: false, - } - queryFlag = Flag{ - Name: "Query", - LongForm: "query", - ShortForm: "q", - Help: "Advanced query.", - IsRequired: false, - } - - ScreenPromptMap = map[string]string{ - "signup-id": "signup-id", - "signup-password": "signup-password", - "login-id": "login-id", - "login-password": "login-password", - "login-passwordless-email-code": "login-passwordless", - "login-passwordless-sms-otp": "login-passwordless", - "phone-identifier-enrollment": "phone-identifier-enrollment", - "phone-identifier-challenge": "phone-identifier-challenge", - "email-identifier-challenge": "email-identifier-challenge", - "passkey-enrollment": "passkeys", - "passkey-enrollment-local": "passkeys", - "interstitial-captcha": "captcha", - "login": "login", - "signup": "signup", - "reset-password-request": "reset-password", - "reset-password-email": "reset-password", - "reset-password": "reset-password", - "reset-password-success": "reset-password", - "reset-password-error": "reset-password", - "reset-password-mfa-email-challenge": "reset-password", - "reset-password-mfa-otp-challenge": "reset-password", - "reset-password-mfa-push-challenge-push": "reset-password", - "reset-password-mfa-sms-challenge": "reset-password", - "reset-password-mfa-phone-challenge": "reset-password", - "reset-password-mfa-voice-challenge": "reset-password", - "reset-password-mfa-recovery-code-challenge": "reset-password", - "reset-password-mfa-webauthn-platform-challenge": "reset-password", - "reset-password-mfa-webauthn-roaming-challenge": "reset-password", - "mfa-detect-browser-capabilities": "mfa", - "mfa-enroll-result": "mfa", - "mfa-begin-enroll-options": "mfa", - "mfa-login-options": "mfa", - "mfa-email-challenge": "mfa-email", - "mfa-email-list": "mfa-email", - "mfa-country-codes": "mfa-sms", - "mfa-sms-challenge": "mfa-sms", - "mfa-sms-enrollment": "mfa-sms", - "mfa-sms-list": "mfa-sms", - "mfa-push-challenge-push": "mfa-push", - "mfa-push-enrollment-qr": "mfa-push", - "mfa-push-list": "mfa-push", - "mfa-push-welcome": "mfa-push", - "accept-invitation": "invitation", - "organization-selection": "organizations", - "organization-picker": "organizations", - "mfa-otp-challenge": "mfa-otp", - "mfa-otp-enrollment-code": "mfa-otp", - "mfa-otp-enrollment-qr": "mfa-otp", - "device-code-activation": "device-flow", - "device-code-activation-allowed": "device-flow", - "device-code-activation-denied": "device-flow", - "device-code-confirmation": "device-flow", - "mfa-phone-challenge": "mfa-phone", - "mfa-phone-enrollment": "mfa-phone", - "mfa-voice-challenge": "mfa-voice", - "mfa-voice-enrollment": "mfa-voice", - "mfa-recovery-code-challenge": "mfa-recovery-code", - "mfa-recovery-code-enrollment": "mfa-recovery-code", - "mfa-recovery-code-challenge-new-code": "mfa-recovery-code", - "redeem-ticket": "common", - "email-verification-result": "email-verification", - "login-email-verification": "login-email-verification", - "logout": "logout", - "logout-aborted": "logout", - "logout-complete": "logout", - "mfa-webauthn-change-key-nickname": "mfa-webauthn", - "mfa-webauthn-enrollment-success": "mfa-webauthn", - "mfa-webauthn-error": "mfa-webauthn", - "mfa-webauthn-platform-challenge": "mfa-webauthn", - "mfa-webauthn-platform-enrollment": "mfa-webauthn", - "mfa-webauthn-roaming-challenge": "mfa-webauthn", - "mfa-webauthn-roaming-enrollment": "mfa-webauthn", - } -) +import "github.com/spf13/cobra" func aculCmd(cli *cli) *cobra.Command { cmd := &cobra.Command{ @@ -180,400 +10,9 @@ func aculCmd(cli *cli) *cobra.Command { } cmd.AddCommand(aculConfigureCmd(cli)) - cmd.AddCommand(aculInitCmd(cli)) + // Check out the ./acul_scaffolding_app.MD file for more information on the commands below. cmd.AddCommand(aculInitCmd1(cli)) cmd.AddCommand(aculInitCmd2(cli)) return cmd } - -type customizationInputs struct { - screenName string - filePath string -} - -func aculConfigureCmd(cli *cli) *cobra.Command { - cmd := &cobra.Command{ - Use: "config", - Short: "Configure the Universal Login experience", - Long: "Configure the Universal Login experience. This requires a custom domain to be configured for the tenant.", - Example: ` auth0 acul config - auth0 acul config - auth0 acul config --screen login-id --file settings.json`, - RunE: func(cmd *cobra.Command, args []string) error { - return advanceCustomize(cmd, cli, customizationInputs{}) - }, - } - - cmd.AddCommand(aculConfigGenerateCmd(cli)) - cmd.AddCommand(aculConfigGet(cli)) - cmd.AddCommand(aculConfigSet(cli)) - cmd.AddCommand(aculConfigListCmd(cli)) - cmd.AddCommand(aculConfigDocsCmd(cli)) - - return cmd -} - -func aculConfigGenerateCmd(cli *cli) *cobra.Command { - var input customizationInputs - - cmd := &cobra.Command{ - Use: "generate", - Args: cobra.MaximumNArgs(1), - Short: "Generate a default rendering config for a screen", - Long: "Generate a default rendering config for a specific screen and save it to a file.", - Example: ` auth0 acul config generate signup-id - auth0 acul config generate login-id --file login-settings.json`, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - cli.renderer.Infof("Please select a screen ") - if err := screenName.Select(cmd, &screenName, utils.FetchKeys(ScreenPromptMap), nil); err != nil { - return handleInputError(err) - } - } else { - input.screenName = args[0] - } - - if input.filePath == "" { - input.filePath = fmt.Sprintf("%s.json", input.screenName) - } - - defaultConfig := map[string]interface{}{ - "rendering_mode": "standard", - "context_configuration": []interface{}{}, - "use_page_template": false, - "default_head_tags_disabled": false, - "head_tags": []interface{}{}, - "filters": []interface{}{}, - } - - data, err := json.MarshalIndent(defaultConfig, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal default config: %w", err) - } - - if err := os.WriteFile(input.filePath, data, 0644); err != nil { - return fmt.Errorf("failed to write config file: %w", err) - } - - cli.renderer.Infof("Configuration successfully generated!\n"+ - " Your new config file is located at ./%s\n"+ - " Review the documentation for configuring screens to use ACUL\n"+ - " https://auth0.com/docs/customize/login-pages/advanced-customizations/getting-started/configure-acul-screens\n", ansi.Green(input.filePath)) - return nil - }, - } - - file.RegisterString(cmd, &input.filePath, "") - - return cmd -} - -func aculConfigGet(cli *cli) *cobra.Command { - var input customizationInputs - - cmd := &cobra.Command{ - Use: "get", - Args: cobra.MaximumNArgs(1), - Short: "Get the current rendering settings for a specific screen", - Long: "Get the current rendering settings for a specific screen.", - Example: ` auth0 acul config get signup-id - auth0 acul config get login-id`, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - cli.renderer.Infof("Please select a screen ") - if err := screenName.Select(cmd, &input.screenName, utils.FetchKeys(ScreenPromptMap), nil); err != nil { - return handleInputError(err) - } - } else { - input.screenName = args[0] - } - - // Fetch existing render settings from the API. - existingRenderSettings, err := cli.api.Prompt.ReadRendering(cmd.Context(), management.PromptType(ScreenPromptMap[input.screenName]), management.ScreenName(input.screenName)) - if err != nil { - return fmt.Errorf("failed to fetch the existing render settings: %w", err) - } - - if input.filePath != "" { - if isFileExists(cli, cmd, input.filePath, input.screenName) { - return nil - } - } else { - cli.renderer.Warnf("No configuration file exists for %s on %s", ansi.Green(input.screenName), ansi.Blue(input.filePath)) - - if !cli.force && canPrompt(cmd) { - message := "Would you like to generate a local config file instead? (Y/n)" - if confirmed := prompt.Confirm(message); !confirmed { - return nil - } - } - - input.filePath = fmt.Sprintf("%s.json", input.screenName) - } - - data, err := json.MarshalIndent(existingRenderSettings, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal render settings: %w", err) - } - - if err := os.WriteFile(input.filePath, data, 0644); err != nil { - return fmt.Errorf("failed to write render settings to file %q: %w", input.filePath, err) - } - - cli.renderer.Infof("Configuration succcessfully downloaded and saved to %s", ansi.Green(input.filePath)) - return nil - }, - } - - screenName.RegisterString(cmd, &input.screenName, "") - file.RegisterString(cmd, &input.filePath, "") - - return cmd -} - -func isFileExists(cli *cli, cmd *cobra.Command, filePath, screen string) bool { - _, err := os.Stat(filePath) - if os.IsNotExist(err) { - return false - } - - cli.renderer.Warnf("A configuration file for %s already exists at %s", ansi.Green(screen), ansi.Blue(filePath)) - - if !cli.force && canPrompt(cmd) { - message := fmt.Sprintf("Overwrite this file with the data from %s? (y/N): ", ansi.Blue(cli.tenant)) - if confirmed := prompt.Confirm(message); !confirmed { - return true - } - } - - return false -} - -func aculConfigSet(cli *cli) *cobra.Command { - var input customizationInputs - - cmd := &cobra.Command{ - Use: "set", - Args: cobra.MaximumNArgs(1), - Short: "Set the rendering settings for a specific screen", - Long: "Set the rendering settings for a specific screen.", - Example: ` auth0 acul config set signup-id --file settings.json - auth0 acul config set login-id --file settings.json`, - RunE: func(cmd *cobra.Command, args []string) error { - return advanceCustomize(cmd, cli, input) - }, - } - - screenName.RegisterString(cmd, &input.screenName, "") - file.RegisterString(cmd, &input.filePath, "") - - return cmd -} - -func advanceCustomize(cmd *cobra.Command, cli *cli, input customizationInputs) error { - var currMode = standardMode - - renderSettings, err := fetchRenderSettings(cmd, cli, input) - if renderSettings != nil && renderSettings.RenderingMode != nil { - currMode = string(*renderSettings.RenderingMode) - } - - if errors.Is(err, ErrNoChangesDetected) { - cli.renderer.Infof("Current rendering mode for Prompt '%s' and Screen '%s': %s", - ansi.Green(ScreenPromptMap[input.screenName]), ansi.Green(input.screenName), ansi.Green(currMode)) - return nil - } - - if err != nil { - return err - } - - if err = ansi.Waiting(func() error { - return cli.api.Prompt.UpdateRendering(cmd.Context(), management.PromptType(ScreenPromptMap[input.screenName]), management.ScreenName(input.screenName), renderSettings) - }); err != nil { - return fmt.Errorf("failed to set the render settings: %w", err) - } - - cli.renderer.Infof( - "Successfully updated the rendering settings.\n Current rendering mode for Prompt '%s' and Screen '%s': %s", - ansi.Green(ScreenPromptMap[input.screenName]), - ansi.Green(input.screenName), - ansi.Green(currMode), - ) - - return nil -} - -func fetchRenderSettings(cmd *cobra.Command, cli *cli, input customizationInputs) (*management.PromptRendering, error) { - var ( - userRenderSettings string - renderSettings = &management.PromptRendering{} - existingSettings = map[string]interface{}{} - currentSettings = map[string]interface{}{} - ) - - if input.filePath != "" { - data, err := os.ReadFile(input.filePath) - if err != nil { - return nil, fmt.Errorf("unable to read file %q: %v", input.filePath, err) - } - - // Validate JSON content. - if err := json.Unmarshal(data, &renderSettings); err != nil { - return nil, fmt.Errorf("file %q contains invalid JSON: %v", input.filePath, err) - } - - return renderSettings, nil - } - - // Fetch existing render settings from the API. - existingRenderSettings, err := cli.api.Prompt.ReadRendering(cmd.Context(), management.PromptType(ScreenPromptMap[input.screenName]), management.ScreenName(input.screenName)) - if err != nil { - return nil, fmt.Errorf("failed to fetch the existing render settings: %w", err) - } - - // Marshal existing render settings into JSON and parse into a map if it's not nil. - if existingRenderSettings != nil { - readRenderingJSON, _ := json.MarshalIndent(existingRenderSettings, "", " ") - if err := json.Unmarshal(readRenderingJSON, &existingSettings); err != nil { - fmt.Println("Error parsing readRendering JSON:", err) - } - } - - existingSettings["___customization guide___"] = "https://github.com/auth0/auth0-cli/blob/main/CUSTOMIZATION_GUIDE.md" - - // Marshal final JSON. - finalJSON, err := json.MarshalIndent(existingSettings, "", " ") - if err != nil { - fmt.Println("Error generating final JSON:", err) - } - - err = rendererScript.OpenEditor(cmd, &userRenderSettings, string(finalJSON), input.screenName+".json", cli.customizeEditorHint) - if err != nil { - return nil, fmt.Errorf("failed to capture input from the editor: %w", err) - } - - // Unmarshal user-provided JSON into a map for comparison. - err = json.Unmarshal([]byte(userRenderSettings), ¤tSettings) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal JSON input into a map: %w", err) - } - - // Compare the existing settings with the updated settings to detect changes. - if reflect.DeepEqual(existingSettings, currentSettings) { - cli.renderer.Warnf("No changes detected in the customization settings. This could be due to uncommitted configuration changes or no modifications being made to the configurations.") - - return existingRenderSettings, ErrNoChangesDetected - } - - if err := json.Unmarshal([]byte(userRenderSettings), &renderSettings); err != nil { - return nil, fmt.Errorf("failed to unmarshal JSON input: %w", err) - } - - return renderSettings, nil -} - -func aculConfigListCmd(cli *cli) *cobra.Command { - var ( - fields string - includeFields bool - includeTotals bool - page int - perPage int - promptName string - screen string - renderingMode string - query string - ) - - cmd := &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, - Short: "List Universal Login rendering configurations", - Long: "List Universal Login rendering configurations with optional filters and pagination.", - Example: ` auth0 acul config list --prompt login-id --screen login --rendering-mode advanced --include-fields true --fields head_tags,context_configuration`, - RunE: func(cmd *cobra.Command, args []string) error { - params := []management.RequestOption{ - management.Parameter("page", strconv.Itoa(page)), - management.Parameter("per_page", strconv.Itoa(perPage)), - } - - if query != "" { - params = append(params, management.Parameter("q", query)) - } - - if includeFields { - if fields != "" { - params = append(params, management.IncludeFields(fields)) - } - } else { - if fields != "" { - params = append(params, management.ExcludeFields(fields)) - } - } - - if screen != "" { - params = append(params, management.Parameter("screen", screen)) - } - - if promptName != "" { - params = append(params, management.Parameter("prompt", promptName)) - } - - if renderingMode != "" { - params = append(params, management.Parameter("rendering_mode", renderingMode)) - } - - var results *management.PromptRenderingList - - if err := ansi.Waiting(func() (err error) { - results, err = cli.api.Prompt.ListRendering(cmd.Context(), params...) - return err - }); err != nil { - return err - } - - fmt.Printf("Results : %v\n", results) - - cli.renderer.ACULConfigList(results) - - return nil - }, - } - - cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") - cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") - - cmd.MarkFlagsMutuallyExclusive("json", "json-compact") - - fieldsFlag.RegisterString(cmd, &fields, "") - includeFieldsFlag.RegisterBool(cmd, &includeFields, true) - includeTotalsFlag.RegisterBool(cmd, &includeTotals, false) - pageFlag.RegisterInt(cmd, &page, 0) - perPageFlag.RegisterInt(cmd, &perPage, 50) - promptFlag.RegisterString(cmd, &promptName, "") - screenFlag.RegisterString(cmd, &screen, "") - renderingModeFlag.RegisterString(cmd, &renderingMode, "") - queryFlag.RegisterString(cmd, &query, "") - - return cmd -} - -func aculConfigDocsCmd(cli *cli) *cobra.Command { - return &cobra.Command{ - Use: "docs", - Short: "Open the ACUL configuration documentation", - Long: "Open the documentation for configuring Advanced Customizations for Universal Login screens.", - Example: ` auth0 acul config docs`, - RunE: func(cmd *cobra.Command, args []string) error { - url := "https://auth0.com/docs/customize/login-pages/advanced-customizations/getting-started/configure-acul-screens" - cli.renderer.Infof("Opening documentation: %s", url) - return browser.OpenURL(url) - }, - } -} - -func (c *cli) customizeEditorHint() { - c.renderer.Infof("%s Once you close the editor, the shown settings will be saved. To cancel, press CTRL+C.", ansi.Faint("Hint:")) -} diff --git a/internal/cli/acul_config.go b/internal/cli/acul_config.go new file mode 100644 index 000000000..e047ac518 --- /dev/null +++ b/internal/cli/acul_config.go @@ -0,0 +1,564 @@ +package cli + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "reflect" + "strconv" + + "github.com/pkg/browser" + + "github.com/auth0/go-auth0/management" + "github.com/spf13/cobra" + + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/prompt" + "github.com/auth0/auth0-cli/internal/utils" +) + +var ( + screenName = Flag{ + Name: "Screen Name", + LongForm: "screen", + ShortForm: "s", + Help: "Name of the screen to customize.", + IsRequired: true, + } + file = Flag{ + Name: "File", + LongForm: "settings-file", + ShortForm: "f", + Help: "File to save the rendering configs to.", + IsRequired: false, + } + rendererScript = Flag{ + Name: "Script", + LongForm: "script", + ShortForm: "s", + Help: "Script contents for the rendering configs.", + IsRequired: true, + } + fieldsFlag = Flag{ + Name: "Fields", + LongForm: "fields", + Help: "Comma-separated list of fields to include or exclude in the result (based on value provided for include_fields) ", + IsRequired: false, + } + includeFieldsFlag = Flag{ + Name: "Include Fields", + LongForm: "include-fields", + Help: "Whether specified fields are to be included (default: true) or excluded (false).", + IsRequired: false, + } + includeTotalsFlag = Flag{ + Name: "Include Totals", + LongForm: "include-totals", + Help: "Return results inside an object that contains the total result count (true) or as a direct array of results (false).", + IsRequired: false, + } + pageFlag = Flag{ + Name: "Page", + LongForm: "page", + Help: "Page index of the results to return. First page is 0.", + IsRequired: false, + } + perPageFlag = Flag{ + Name: "Per Page", + LongForm: "per-page", + Help: "Number of results per page. Default value is 50, maximum value is 100.", + IsRequired: false, + } + promptFlag = Flag{ + Name: "Prompt", + LongForm: "prompt", + Help: "Filter by the Universal Login prompt.", + IsRequired: false, + } + screenFlag = Flag{ + Name: "Screen", + LongForm: "screen", + Help: "Filter by the Universal Login screen.", + IsRequired: false, + } + renderingModeFlag = Flag{ + Name: "Rendering Mode", + LongForm: "rendering-mode", + Help: "Filter by the rendering mode (advanced or standard).", + IsRequired: false, + } + queryFlag = Flag{ + Name: "Query", + LongForm: "query", + ShortForm: "q", + Help: "Advanced query.", + IsRequired: false, + } + + ScreenPromptMap = map[string]string{ + "signup-id": "signup-id", + "signup-password": "signup-password", + "login-id": "login-id", + "login-password": "login-password", + "login-passwordless-email-code": "login-passwordless", + "login-passwordless-sms-otp": "login-passwordless", + "phone-identifier-enrollment": "phone-identifier-enrollment", + "phone-identifier-challenge": "phone-identifier-challenge", + "email-identifier-challenge": "email-identifier-challenge", + "passkey-enrollment": "passkeys", + "passkey-enrollment-local": "passkeys", + "interstitial-captcha": "captcha", + "login": "login", + "signup": "signup", + "reset-password-request": "reset-password", + "reset-password-email": "reset-password", + "reset-password": "reset-password", + "reset-password-success": "reset-password", + "reset-password-error": "reset-password", + "reset-password-mfa-email-challenge": "reset-password", + "reset-password-mfa-otp-challenge": "reset-password", + "reset-password-mfa-push-challenge-push": "reset-password", + "reset-password-mfa-sms-challenge": "reset-password", + "reset-password-mfa-phone-challenge": "reset-password", + "reset-password-mfa-voice-challenge": "reset-password", + "reset-password-mfa-recovery-code-challenge": "reset-password", + "reset-password-mfa-webauthn-platform-challenge": "reset-password", + "reset-password-mfa-webauthn-roaming-challenge": "reset-password", + "mfa-detect-browser-capabilities": "mfa", + "mfa-enroll-result": "mfa", + "mfa-begin-enroll-options": "mfa", + "mfa-login-options": "mfa", + "mfa-email-challenge": "mfa-email", + "mfa-email-list": "mfa-email", + "mfa-country-codes": "mfa-sms", + "mfa-sms-challenge": "mfa-sms", + "mfa-sms-enrollment": "mfa-sms", + "mfa-sms-list": "mfa-sms", + "mfa-push-challenge-push": "mfa-push", + "mfa-push-enrollment-qr": "mfa-push", + "mfa-push-list": "mfa-push", + "mfa-push-welcome": "mfa-push", + "accept-invitation": "invitation", + "organization-selection": "organizations", + "organization-picker": "organizations", + "mfa-otp-challenge": "mfa-otp", + "mfa-otp-enrollment-code": "mfa-otp", + "mfa-otp-enrollment-qr": "mfa-otp", + "device-code-activation": "device-flow", + "device-code-activation-allowed": "device-flow", + "device-code-activation-denied": "device-flow", + "device-code-confirmation": "device-flow", + "mfa-phone-challenge": "mfa-phone", + "mfa-phone-enrollment": "mfa-phone", + "mfa-voice-challenge": "mfa-voice", + "mfa-voice-enrollment": "mfa-voice", + "mfa-recovery-code-challenge": "mfa-recovery-code", + "mfa-recovery-code-enrollment": "mfa-recovery-code", + "mfa-recovery-code-challenge-new-code": "mfa-recovery-code", + "redeem-ticket": "common", + "email-verification-result": "email-verification", + "login-email-verification": "login-email-verification", + "logout": "logout", + "logout-aborted": "logout", + "logout-complete": "logout", + "mfa-webauthn-change-key-nickname": "mfa-webauthn", + "mfa-webauthn-enrollment-success": "mfa-webauthn", + "mfa-webauthn-error": "mfa-webauthn", + "mfa-webauthn-platform-challenge": "mfa-webauthn", + "mfa-webauthn-platform-enrollment": "mfa-webauthn", + "mfa-webauthn-roaming-challenge": "mfa-webauthn", + "mfa-webauthn-roaming-enrollment": "mfa-webauthn", + } +) + +type customizationInputs struct { + screenName string + filePath string +} + +func aculConfigureCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "config", + Short: "Configure the Universal Login experience", + Long: "Configure the Universal Login experience. This requires a custom domain to be configured for the tenant.", + Example: ` auth0 acul config + auth0 acul config + auth0 acul config --screen login-id --file settings.json`, + RunE: func(cmd *cobra.Command, args []string) error { + return advanceCustomize(cmd, cli, customizationInputs{}) + }, + } + + cmd.AddCommand(aculConfigGenerateCmd(cli)) + cmd.AddCommand(aculConfigGet(cli)) + cmd.AddCommand(aculConfigSet(cli)) + cmd.AddCommand(aculConfigListCmd(cli)) + cmd.AddCommand(aculConfigDocsCmd(cli)) + + return cmd +} + +func aculConfigGenerateCmd(cli *cli) *cobra.Command { + var input customizationInputs + + cmd := &cobra.Command{ + Use: "generate", + Args: cobra.MaximumNArgs(1), + Short: "Generate a default rendering config for a screen", + Long: "Generate a default rendering config for a specific screen and save it to a file.", + Example: ` auth0 acul config generate signup-id + auth0 acul config generate login-id --file login-settings.json`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + cli.renderer.Infof("Please select a screen ") + if err := screenName.Select(cmd, &screenName, utils.FetchKeys(ScreenPromptMap), nil); err != nil { + return handleInputError(err) + } + } else { + input.screenName = args[0] + } + + if input.filePath == "" { + input.filePath = fmt.Sprintf("%s.json", input.screenName) + } + + defaultConfig := map[string]interface{}{ + "rendering_mode": "standard", + "context_configuration": []interface{}{}, + "use_page_template": false, + "default_head_tags_disabled": false, + "head_tags": []interface{}{}, + "filters": []interface{}{}, + } + + data, err := json.MarshalIndent(defaultConfig, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal default config: %w", err) + } + + if err := os.WriteFile(input.filePath, data, 0644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + cli.renderer.Infof("Configuration successfully generated!\n"+ + " Your new config file is located at ./%s\n"+ + " Review the documentation for configuring screens to use ACUL\n"+ + " https://auth0.com/docs/customize/login-pages/advanced-customizations/getting-started/configure-acul-screens\n", ansi.Green(input.filePath)) + return nil + }, + } + + file.RegisterString(cmd, &input.filePath, "") + + return cmd +} + +func aculConfigGet(cli *cli) *cobra.Command { + var input customizationInputs + + cmd := &cobra.Command{ + Use: "get", + Args: cobra.MaximumNArgs(1), + Short: "Get the current rendering settings for a specific screen", + Long: "Get the current rendering settings for a specific screen.", + Example: ` auth0 acul config get signup-id + auth0 acul config get login-id`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + cli.renderer.Infof("Please select a screen ") + if err := screenName.Select(cmd, &input.screenName, utils.FetchKeys(ScreenPromptMap), nil); err != nil { + return handleInputError(err) + } + } else { + input.screenName = args[0] + } + + // Fetch existing render settings from the API. + existingRenderSettings, err := cli.api.Prompt.ReadRendering(cmd.Context(), management.PromptType(ScreenPromptMap[input.screenName]), management.ScreenName(input.screenName)) + if err != nil { + return fmt.Errorf("failed to fetch the existing render settings: %w", err) + } + + if input.filePath != "" { + if isFileExists(cli, cmd, input.filePath, input.screenName) { + return nil + } + } else { + cli.renderer.Warnf("No configuration file exists for %s on %s", ansi.Green(input.screenName), ansi.Blue(input.filePath)) + + if !cli.force && canPrompt(cmd) { + message := "Would you like to generate a local config file instead? (Y/n)" + if confirmed := prompt.Confirm(message); !confirmed { + return nil + } + } + + input.filePath = fmt.Sprintf("%s.json", input.screenName) + } + + data, err := json.MarshalIndent(existingRenderSettings, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal render settings: %w", err) + } + + if err := os.WriteFile(input.filePath, data, 0644); err != nil { + return fmt.Errorf("failed to write render settings to file %q: %w", input.filePath, err) + } + + cli.renderer.Infof("Configuration succcessfully downloaded and saved to %s", ansi.Green(input.filePath)) + return nil + }, + } + + screenName.RegisterString(cmd, &input.screenName, "") + file.RegisterString(cmd, &input.filePath, "") + + return cmd +} + +func isFileExists(cli *cli, cmd *cobra.Command, filePath, screen string) bool { + _, err := os.Stat(filePath) + if os.IsNotExist(err) { + return false + } + + cli.renderer.Warnf("A configuration file for %s already exists at %s", ansi.Green(screen), ansi.Blue(filePath)) + + if !cli.force && canPrompt(cmd) { + message := fmt.Sprintf("Overwrite this file with the data from %s? (y/N): ", ansi.Blue(cli.tenant)) + if confirmed := prompt.Confirm(message); !confirmed { + return true + } + } + + return false +} + +func aculConfigSet(cli *cli) *cobra.Command { + var input customizationInputs + + cmd := &cobra.Command{ + Use: "set", + Args: cobra.MaximumNArgs(1), + Short: "Set the rendering settings for a specific screen", + Long: "Set the rendering settings for a specific screen.", + Example: ` auth0 acul config set signup-id --file settings.json + auth0 acul config set login-id --file settings.json`, + RunE: func(cmd *cobra.Command, args []string) error { + return advanceCustomize(cmd, cli, input) + }, + } + + screenName.RegisterString(cmd, &input.screenName, "") + file.RegisterString(cmd, &input.filePath, "") + + return cmd +} + +func advanceCustomize(cmd *cobra.Command, cli *cli, input customizationInputs) error { + var currMode = standardMode + + renderSettings, err := fetchRenderSettings(cmd, cli, input) + if renderSettings != nil && renderSettings.RenderingMode != nil { + currMode = string(*renderSettings.RenderingMode) + } + + if errors.Is(err, ErrNoChangesDetected) { + cli.renderer.Infof("Current rendering mode for Prompt '%s' and Screen '%s': %s", + ansi.Green(ScreenPromptMap[input.screenName]), ansi.Green(input.screenName), ansi.Green(currMode)) + return nil + } + + if err != nil { + return err + } + + if err = ansi.Waiting(func() error { + return cli.api.Prompt.UpdateRendering(cmd.Context(), management.PromptType(ScreenPromptMap[input.screenName]), management.ScreenName(input.screenName), renderSettings) + }); err != nil { + return fmt.Errorf("failed to set the render settings: %w", err) + } + + cli.renderer.Infof( + "Successfully updated the rendering settings.\n Current rendering mode for Prompt '%s' and Screen '%s': %s", + ansi.Green(ScreenPromptMap[input.screenName]), + ansi.Green(input.screenName), + ansi.Green(currMode), + ) + + return nil +} + +func fetchRenderSettings(cmd *cobra.Command, cli *cli, input customizationInputs) (*management.PromptRendering, error) { + var ( + userRenderSettings string + renderSettings = &management.PromptRendering{} + existingSettings = map[string]interface{}{} + currentSettings = map[string]interface{}{} + ) + + if input.filePath != "" { + data, err := os.ReadFile(input.filePath) + if err != nil { + return nil, fmt.Errorf("unable to read file %q: %v", input.filePath, err) + } + + // Validate JSON content. + if err := json.Unmarshal(data, &renderSettings); err != nil { + return nil, fmt.Errorf("file %q contains invalid JSON: %v", input.filePath, err) + } + + return renderSettings, nil + } + + // Fetch existing render settings from the API. + existingRenderSettings, err := cli.api.Prompt.ReadRendering(cmd.Context(), management.PromptType(ScreenPromptMap[input.screenName]), management.ScreenName(input.screenName)) + if err != nil { + return nil, fmt.Errorf("failed to fetch the existing render settings: %w", err) + } + + // Marshal existing render settings into JSON and parse into a map if it's not nil. + if existingRenderSettings != nil { + readRenderingJSON, _ := json.MarshalIndent(existingRenderSettings, "", " ") + if err := json.Unmarshal(readRenderingJSON, &existingSettings); err != nil { + fmt.Println("Error parsing readRendering JSON:", err) + } + } + + existingSettings["___customization guide___"] = "https://github.com/auth0/auth0-cli/blob/main/CUSTOMIZATION_GUIDE.md" + + // Marshal final JSON. + finalJSON, err := json.MarshalIndent(existingSettings, "", " ") + if err != nil { + fmt.Println("Error generating final JSON:", err) + } + + err = rendererScript.OpenEditor(cmd, &userRenderSettings, string(finalJSON), input.screenName+".json", cli.customizeEditorHint) + if err != nil { + return nil, fmt.Errorf("failed to capture input from the editor: %w", err) + } + + // Unmarshal user-provided JSON into a map for comparison. + err = json.Unmarshal([]byte(userRenderSettings), ¤tSettings) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON input into a map: %w", err) + } + + // Compare the existing settings with the updated settings to detect changes. + if reflect.DeepEqual(existingSettings, currentSettings) { + cli.renderer.Warnf("No changes detected in the customization settings. This could be due to uncommitted configuration changes or no modifications being made to the configurations.") + + return existingRenderSettings, ErrNoChangesDetected + } + + if err := json.Unmarshal([]byte(userRenderSettings), &renderSettings); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON input: %w", err) + } + + return renderSettings, nil +} + +func aculConfigListCmd(cli *cli) *cobra.Command { + var ( + fields string + includeFields bool + includeTotals bool + page int + perPage int + promptName string + screen string + renderingMode string + query string + ) + + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List Universal Login rendering configurations", + Long: "List Universal Login rendering configurations with optional filters and pagination.", + Example: ` auth0 acul config list --prompt login-id --screen login --rendering-mode advanced --include-fields true --fields head_tags,context_configuration`, + RunE: func(cmd *cobra.Command, args []string) error { + params := []management.RequestOption{ + management.Parameter("page", strconv.Itoa(page)), + management.Parameter("per_page", strconv.Itoa(perPage)), + } + + if query != "" { + params = append(params, management.Parameter("q", query)) + } + + if includeFields { + if fields != "" { + params = append(params, management.IncludeFields(fields)) + } + } else { + if fields != "" { + params = append(params, management.ExcludeFields(fields)) + } + } + + if screen != "" { + params = append(params, management.Parameter("screen", screen)) + } + + if promptName != "" { + params = append(params, management.Parameter("prompt", promptName)) + } + + if renderingMode != "" { + params = append(params, management.Parameter("rendering_mode", renderingMode)) + } + + var results *management.PromptRenderingList + + if err := ansi.Waiting(func() (err error) { + results, err = cli.api.Prompt.ListRendering(cmd.Context(), params...) + return err + }); err != nil { + return err + } + + fmt.Printf("Results : %v\n", results) + + cli.renderer.ACULConfigList(results) + + return nil + }, + } + + cmd.Flags().BoolVar(&cli.json, "json", false, "Output in json format.") + cmd.Flags().BoolVar(&cli.jsonCompact, "json-compact", false, "Output in compact json format.") + + cmd.MarkFlagsMutuallyExclusive("json", "json-compact") + + fieldsFlag.RegisterString(cmd, &fields, "") + includeFieldsFlag.RegisterBool(cmd, &includeFields, true) + includeTotalsFlag.RegisterBool(cmd, &includeTotals, false) + pageFlag.RegisterInt(cmd, &page, 0) + perPageFlag.RegisterInt(cmd, &perPage, 50) + promptFlag.RegisterString(cmd, &promptName, "") + screenFlag.RegisterString(cmd, &screen, "") + renderingModeFlag.RegisterString(cmd, &renderingMode, "") + queryFlag.RegisterString(cmd, &query, "") + + return cmd +} + +func aculConfigDocsCmd(cli *cli) *cobra.Command { + return &cobra.Command{ + Use: "docs", + Short: "Open the ACUL configuration documentation", + Long: "Open the documentation for configuring Advanced Customizations for Universal Login screens.", + Example: ` auth0 acul config docs`, + RunE: func(cmd *cobra.Command, args []string) error { + url := "https://auth0.com/docs/customize/login-pages/advanced-customizations/getting-started/configure-acul-screens" + cli.renderer.Infof("Opening documentation: %s", url) + return browser.OpenURL(url) + }, + } +} + +func (c *cli) customizeEditorHint() { + c.renderer.Infof("%s Once you close the editor, the shown settings will be saved. To cancel, press CTRL+C.", ansi.Faint("Hint:")) +} diff --git a/internal/cli/acul_sacffolding_app.MD b/internal/cli/acul_sacffolding_app.MD index 7089e66f6..c693f17cb 100644 --- a/internal/cli/acul_sacffolding_app.MD +++ b/internal/cli/acul_sacffolding_app.MD @@ -32,6 +32,7 @@ Uses the GitHub Tree API to enumerate files and `raw.githubusercontent.com` to d **Cons:** - Many HTTP requests; slower and susceptible to GitHub API rate limits. - Loses executable bits and some metadata unless explicitly restored. +- Takes more time to download many small files [so, removed in favor of Method C]. --- diff --git a/internal/cli/acul_sca.go b/internal/cli/acul_sca.go index 4a3582903..499b7bc47 100644 --- a/internal/cli/acul_sca.go +++ b/internal/cli/acul_sca.go @@ -2,8 +2,6 @@ package cli import ( "fmt" - "github.com/auth0/auth0-cli/internal/utils" - "github.com/spf13/cobra" "io" "log" "net/http" @@ -11,11 +9,22 @@ import ( "path/filepath" "time" + "github.com/spf13/cobra" + "github.com/auth0/auth0-cli/internal/prompt" + "github.com/auth0/auth0-cli/internal/utils" ) +var templateFlag = Flag{ + Name: "Template", + LongForm: "template", + ShortForm: "t", + Help: "Name of the template to use", + IsRequired: false, +} + // This logic goes inside your `RunE` function. -func aculInitCmd2(c *cli) *cobra.Command { +func aculInitCmd2(_ *cli) *cobra.Command { cmd := &cobra.Command{ Use: "init2", Args: cobra.MaximumNArgs(1), @@ -25,42 +34,34 @@ func aculInitCmd2(c *cli) *cobra.Command { } return cmd - } func runScaffold2(cmd *cobra.Command, args []string) error { - // Step 1: fetch manifest.json + // Step 1: fetch manifest.json. manifest, err := fetchManifest() if err != nil { return err } - // Step 2: select template - var templateNames []string - for k := range manifest.Templates { - templateNames = append(templateNames, k) + var chosenTemplate string + if err := templateFlag.Select(cmd, &chosenTemplate, utils.FetchKeys(manifest.Templates), nil); err != nil { + return handleInputError(err) } - var chosen string - promptText := prompt.SelectInput("", "Select a template", "Chosen template(Todo)", utils.FetchKeys(manifest.Templates), "react-js", true) - if err := prompt.AskOne(promptText, &chosen); err != nil { - fmt.Println(err) - } - - // Step 3: select screens + // Step 3: select screens. var screenOptions []string - template := manifest.Templates[chosen] + template := manifest.Templates[chosenTemplate] for _, s := range template.Screens { screenOptions = append(screenOptions, s.ID) } - // Step 3: Let user select screens + // Step 3: Let user select screens. var selectedScreens []string if err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...); err != nil { return err } - // Step 3: Create project folder + // Step 3: Create project folder. var destDir string if len(args) < 1 { destDir = "my_acul_proj2" @@ -73,56 +74,66 @@ func runScaffold2(cmd *cobra.Command, args []string) error { curr := time.Now() - // --- Step 1: Download and Unzip to Temp Dir --- + // --- Step 1: Download and Unzip to Temp Dir ---. repoURL := "https://github.com/auth0-samples/auth0-acul-samples/archive/refs/heads/monorepo-sample.zip" tempZipFile := downloadFile(repoURL) - defer os.Remove(tempZipFile) // Clean up the temp zip file + defer os.Remove(tempZipFile) // Clean up the temp zip file. tempUnzipDir, err := os.MkdirTemp("", "unzipped-repo-*") check(err, "Error creating temporary unzipped directory") - defer os.RemoveAll(tempUnzipDir) // Clean up the entire temp directory + defer os.RemoveAll(tempUnzipDir) // Clean up the entire temp directory. err = utils.Unzip(tempZipFile, tempUnzipDir) if err != nil { return err } - // TODO: Adjust this prefix based on the actual structure of the unzipped content(once main branch is used) - const sourcePathPrefix = "auth0-acul-samples-monorepo-sample/" + // TODO: Adjust this prefix based on the actual structure of the unzipped content(once main branch is used). + var sourcePathPrefix = "auth0-acul-samples-monorepo-sample/" + chosenTemplate + + // --- Step 2: Copy the Specified Base Directories ---. + for _, dir := range manifest.Templates[chosenTemplate].BaseDirectories { + // TODO: Remove hardcoding of removing the template - instead ensure to remove the template name in sourcePathPrefix. + relPath, err := filepath.Rel(chosenTemplate, dir) + if err != nil { + continue + } - // --- Step 2: Copy the Specified Base Directories --- - for _, dir := range manifest.Templates[chosen].BaseDirectories { - srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, dir) - destPath := filepath.Join(destDir, dir) + srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, relPath) + destPath := filepath.Join(destDir, relPath) - if _, err := os.Stat(srcPath); os.IsNotExist(err) { + if _, err = os.Stat(srcPath); os.IsNotExist(err) { log.Printf("Warning: Source directory does not exist: %s", srcPath) continue } - fmt.Printf("Copying directory: %s\n", dir) - err := copyDir(srcPath, destPath) + err = copyDir(srcPath, destPath) check(err, fmt.Sprintf("Error copying directory %s", dir)) } - // --- Step 3: Copy the Specified Base Files --- - for _, baseFile := range manifest.Templates[chosen].BaseFiles { - srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, baseFile) - destPath := filepath.Join(destDir, baseFile) + // --- Step 3: Copy the Specified Base Files ---. + for _, baseFile := range manifest.Templates[chosenTemplate].BaseFiles { + // TODO: Remove hardcoding of removing the template - instead ensure to remove the template name in sourcePathPrefix. + relPath, err := filepath.Rel(chosenTemplate, baseFile) + if err != nil { + continue + } + + srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, relPath) + destPath := filepath.Join(destDir, relPath) if _, err = os.Stat(srcPath); os.IsNotExist(err) { log.Printf("Warning: Source file does not exist: %s", srcPath) continue } - //parentDir := filepath.Dir(destPath) - //if err := os.MkdirAll(parentDir, 0755); err != nil { - // log.Printf("Error creating parent directory for %s: %v", baseFile, err) - // continue - //} + parentDir := filepath.Dir(destPath) + if err := os.MkdirAll(parentDir, 0755); err != nil { + log.Printf("Error creating parent directory for %s: %v", baseFile, err) + continue + } - fmt.Printf("Copying file: %s\n", baseFile) - err := copyFile(srcPath, destPath) + err = copyFile(srcPath, destPath) check(err, fmt.Sprintf("Error copying file %s", baseFile)) } @@ -130,41 +141,46 @@ func runScaffold2(cmd *cobra.Command, args []string) error { for _, s := range selectedScreens { screen := screenInfo[s] - srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, screen.Path) - destPath := filepath.Join(destDir, screen.Path) + relPath, err := filepath.Rel(chosenTemplate, screen.Path) + if err != nil { + continue + } + + srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, relPath) + destPath := filepath.Join(destDir, relPath) if _, err = os.Stat(srcPath); os.IsNotExist(err) { log.Printf("Warning: Source directory does not exist: %s", srcPath) continue } - //parentDir := filepath.Dir(destPath) - //if err := os.MkdirAll(parentDir, 0755); err != nil { - // log.Printf("Error creating parent directory for %s: %v", screen.Path, err) - // continue - //} + parentDir := filepath.Dir(destPath) + if err := os.MkdirAll(parentDir, 0755); err != nil { + log.Printf("Error creating parent directory for %s: %v", screen.Path, err) + continue + } - fmt.Printf("Copying screen file: %s\n", screen.Path) - err := copyFile(srcPath, destPath) + fmt.Printf("Copying screen path: %s\n", screen.Path) + err = copyDir(srcPath, destPath) check(err, fmt.Sprintf("Error copying screen file %s", screen.Path)) - } - fmt.Println("\nSuccess! The files and directories have been copied.") - fmt.Println(time.Since(curr)) + fmt.Println("\nProject successfully created!\n" + + "Explore the sample app: https://github.com/auth0/acul-sample-app") + return nil } -// Helper function to handle errors and log them +// Helper function to handle errors and log them. func check(err error, msg string) { if err != nil { log.Fatalf("%s: %v", err, msg) } } -// Function to download a file from a URL to a temporary location +// Function to download a file from a URL to a temporary location. func downloadFile(url string) string { tempFile, err := os.CreateTemp("", "github-zip-*.zip") check(err, "Error creating temporary file") @@ -175,7 +191,7 @@ func downloadFile(url string) string { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - log.Fatalf("Bad status code: %s", resp.Status) + log.Printf("Bad status code: %s", resp.Status) } _, err = io.Copy(tempFile, resp.Body) @@ -185,7 +201,7 @@ func downloadFile(url string) string { return tempFile.Name() } -// Function to copy a file from a source path to a destination path +// Function to copy a file from a source path to a destination path. func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { @@ -206,7 +222,7 @@ func copyFile(src, dst string) error { return out.Close() } -// Function to recursively copy a directory +// Function to recursively copy a directory. func copyDir(src, dst string) error { sourceInfo, err := os.Stat(src) if err != nil { @@ -242,7 +258,8 @@ func copyDir(src, dst string) error { func createScreenMap(screens []Screen) map[string]Screen { screenMap := make(map[string]Screen) for _, screen := range screens { - screenMap[screen.Name] = screen + screenMap[screen.ID] = screen } + return screenMap } diff --git a/internal/cli/acul_scaff.go b/internal/cli/acul_scaff.go index d501b881b..f4186f73e 100644 --- a/internal/cli/acul_scaff.go +++ b/internal/cli/acul_scaff.go @@ -1,19 +1,79 @@ package cli import ( + "encoding/json" "fmt" - "github.com/auth0/auth0-cli/internal/utils" - "github.com/spf13/cobra" + "io" + "net/http" "os" "os/exec" "path/filepath" "time" + "github.com/spf13/cobra" + "github.com/auth0/auth0-cli/internal/prompt" + "github.com/auth0/auth0-cli/internal/utils" ) +type Manifest struct { + Templates map[string]Template `json:"templates"` + Metadata Metadata `json:"metadata"` +} + +type Template struct { + Name string `json:"name"` + Description string `json:"description"` + Framework string `json:"framework"` + SDK string `json:"sdk"` + BaseFiles []string `json:"base_files"` + BaseDirectories []string `json:"base_directories"` + Screens []Screen `json:"screens"` +} + +type Screen struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Path string `json:"path"` +} + +type Metadata struct { + Version string `json:"version"` + Repository string `json:"repository"` + LastUpdated string `json:"last_updated"` + Description string `json:"description"` +} + +func fetchManifest() (*Manifest, error) { + // The URL to the raw JSON file in the repository. + url := "https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/monorepo-sample/manifest.json" + + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("cannot fetch manifest: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch manifest: received status code %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("cannot read manifest body: %w", err) + } + + var manifest Manifest + if err := json.Unmarshal(body, &manifest); err != nil { + return nil, fmt.Errorf("invalid manifest format: %w", err) + } + + return &manifest, nil +} + // This logic goes inside your `RunE` function. -func aculInitCmd1(c *cli) *cobra.Command { +func aculInitCmd1(_ *cli) *cobra.Command { cmd := &cobra.Command{ Use: "init1", Args: cobra.MaximumNArgs(1), @@ -23,43 +83,36 @@ func aculInitCmd1(c *cli) *cobra.Command { } return cmd - } func runScaffold(cmd *cobra.Command, args []string) error { - - // Step 1: fetch manifest.json + // Step 1: fetch manifest.json. manifest, err := fetchManifest() if err != nil { return err } - // Step 2: select template - templateNames := []string{} - for k := range manifest.Templates { - templateNames = append(templateNames, k) - } - + // Step 2: select template. var chosen string promptText := prompt.SelectInput("", "Select a template", "Chosen template(Todo)", utils.FetchKeys(manifest.Templates), "react-js", true) - if err := prompt.AskOne(promptText, &chosen); err != nil { - + if err = prompt.AskOne(promptText, &chosen); err != nil { + return err } - // Step 3: select screens + // Step 3: select screens. var screenOptions []string template := manifest.Templates[chosen] for _, s := range template.Screens { screenOptions = append(screenOptions, s.ID) } - // Step 3: Let user select screens + // Step 3: Let user select screens. var selectedScreens []string if err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...); err != nil { return err } - // Step 3: Create project folder + // Step 3: Create project folder. var projectDir string if len(args) < 1 { projectDir = "my_acul_proj1" @@ -72,8 +125,7 @@ func runScaffold(cmd *cobra.Command, args []string) error { curr := time.Now() - // Step 4: Init git repo - + // Step 4: Init git repo. repoURL := "https://github.com/auth0-samples/auth0-acul-samples.git" if err := runGit(projectDir, "init"); err != nil { return err @@ -85,16 +137,17 @@ func runScaffold(cmd *cobra.Command, args []string) error { return err } - // Step 5: Write sparse-checkout paths + // Step 5: Write sparse-checkout paths. baseFiles := manifest.Templates[chosen].BaseFiles baseDirectories := manifest.Templates[chosen].BaseDirectories - paths := append(baseFiles, baseDirectories...) - paths = append(paths, selectedScreens...) + var paths []string + paths = append(paths, baseFiles...) + paths = append(paths, baseDirectories...) for _, scr := range template.Screens { for _, chosenScreen := range selectedScreens { - if scr.Name == chosenScreen { + if scr.ID == chosenScreen { paths = append(paths, scr.Path) } } @@ -113,15 +166,15 @@ func runScaffold(cmd *cobra.Command, args []string) error { f.Close() - // Step 6: Pull only sparse files + // Step 6: Pull only sparse files. if err := runGit(projectDir, "pull", "origin", "monorepo-sample"); err != nil { return err } - // Step 7: Clean up .git - //if err := os.RemoveAll(filepath.Join(projectDir, ".git")); err != nil { - // return fmt.Errorf("failed to clean up git metadata: %w", err) - //} + // Step 7: Clean up .git. + if err := os.RemoveAll(filepath.Join(projectDir, ".git")); err != nil { + return fmt.Errorf("failed to clean up git metadata: %w", err) + } fmt.Println(time.Since(curr)) diff --git a/internal/cli/acul_scaffolding.go b/internal/cli/acul_scaffolding.go deleted file mode 100644 index 4ce681140..000000000 --- a/internal/cli/acul_scaffolding.go +++ /dev/null @@ -1,218 +0,0 @@ -package cli - -import ( - "encoding/json" - "fmt" - "github.com/auth0/auth0-cli/internal/prompt" - "github.com/auth0/auth0-cli/internal/utils" - "github.com/spf13/cobra" - "io" - "net/http" - "os" - "path/filepath" - "time" -) - -type Manifest struct { - Templates map[string]Template `json:"templates"` - Metadata Metadata `json:"metadata"` -} - -type Template struct { - Name string `json:"name"` - Description string `json:"description"` - Framework string `json:"framework"` - SDK string `json:"sdk"` - BaseFiles []string `json:"base_files"` - BaseDirectories []string `json:"base_directories"` - Screens []Screen `json:"screens"` -} - -type Screen struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Path string `json:"path"` -} - -type Metadata struct { - Version string `json:"version"` - Repository string `json:"repository"` - LastUpdated string `json:"last_updated"` - Description string `json:"description"` -} - -// raw GitHub base URL -const rawBaseURL = "https://raw.githubusercontent.com" - -func main() { - -} - -func fetchManifest() (*Manifest, error) { - // The URL to the raw JSON file in the repository. - url := "https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/monorepo-sample/manifest.json" - - resp, err := http.Get(url) - if err != nil { - return nil, fmt.Errorf("cannot fetch manifest: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to fetch manifest: received status code %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("cannot read manifest body: %w", err) - } - - var manifest Manifest - if err := json.Unmarshal(body, &manifest); err != nil { - return nil, fmt.Errorf("invalid manifest format: %w", err) - } - - return &manifest, nil -} - -// This logic goes inside your `RunE` function. -func aculInitCmd(c *cli) *cobra.Command { - cmd := &cobra.Command{ - Use: "init", - Args: cobra.MaximumNArgs(1), - Short: "Generate a new project from a template", - Long: `Generate a new project from a template.`, - RunE: func(cmd *cobra.Command, args []string) error { - - manifest, err := fetchManifest() - if err != nil { - return err - } - - // Step 2: select template - var templateNames []string - for k := range manifest.Templates { - templateNames = append(templateNames, k) - } - - var chosen string - promptText := prompt.SelectInput("", "Select a template", "Chosen template(Todo)", utils.FetchKeys(manifest.Templates), "react-js", true) - if err := prompt.AskOne(promptText, &chosen); err != nil { - - } - - // Step 3: select screens - var screenOptions []string - template := manifest.Templates[chosen] - for _, s := range template.Screens { - screenOptions = append(screenOptions, s.ID) - } - - // Step 3: Let user select screens - var selectedScreens []string - if err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...); err != nil { - return err - } - - var targetRoot string - if len(args) < 1 { - targetRoot = "my_acul_proj" - } else { - targetRoot = args[0] - } - - if err := os.MkdirAll(targetRoot, 0755); err != nil { - return fmt.Errorf("failed to create project dir: %w", err) - } - - curr := time.Now() - - fmt.Println(time.Since(curr)) - - fmt.Println("✅ Scaffolding complete") - - return nil - }, - } - - return cmd - -} - -const baseRawURL = "https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/monorepo-sample" - -// GitHub API base for directory traversal -const baseTreeAPI = "https://api.github.com/repos/auth0-samples/auth0-acul-samples/git/trees/monorepo-sample?recursive=1" - -// downloadRaw fetches a single file and saves it locally. -func downloadRaw(path, destDir string) error { - url := fmt.Sprintf("%s/%s", baseRawURL, path) - resp, err := http.Get(url) - if err != nil { - return fmt.Errorf("failed to fetch %s: %w", url, err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("failed to fetch %s: %s", url, resp.Status) - } - - // Create destination path - destPath := filepath.Join(destDir, path) - if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { - return fmt.Errorf("failed to create dirs for %s: %w", destPath, err) - } - - // Write file - out, err := os.Create(destPath) - if err != nil { - return fmt.Errorf("failed to create file %s: %w", destPath, err) - } - defer out.Close() - - _, err = io.Copy(out, resp.Body) - if err != nil { - return fmt.Errorf("failed to write %s: %w", destPath, err) - } - - return nil -} - -// GitHub tree API response -type treeEntry struct { - Path string `json:"path"` - Type string `json:"type"` // "blob" (file) or "tree" (dir) - URL string `json:"url"` -} - -type treeResponse struct { - Tree []treeEntry `json:"tree"` -} - -// downloadDirectory downloads all files under a given directory using GitHub Tree API. -func downloadDirectory(dir, destDir string) error { - resp, err := http.Get(baseTreeAPI) - if err != nil { - return fmt.Errorf("failed to fetch tree: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("failed to fetch tree API: %s", resp.Status) - } - - var tr treeResponse - if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil { - return fmt.Errorf("failed to decode tree: %w", err) - } - - for _, entry := range tr.Tree { - if entry.Type == "blob" && filepath.HasPrefix(entry.Path, dir) { - if err := downloadRaw(entry.Path, destDir); err != nil { - return err - } - } - } - return nil -} From b6632401f61bed3c66df33f01a5ca6e34ceee186 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Sun, 7 Sep 2025 22:58:36 +0530 Subject: [PATCH 12/58] add acul_config generation for acul app init --- internal/cli/acul_sca.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/internal/cli/acul_sca.go b/internal/cli/acul_sca.go index 499b7bc47..5f00b2d46 100644 --- a/internal/cli/acul_sca.go +++ b/internal/cli/acul_sca.go @@ -1,6 +1,7 @@ package cli import ( + "encoding/json" "fmt" "io" "log" @@ -167,6 +168,26 @@ func runScaffold2(cmd *cobra.Command, args []string) error { fmt.Println(time.Since(curr)) + config := AculConfig{ + ChosenTemplate: chosenTemplate, + Screen: selectedScreens, // If needed + InitTimestamp: time.Now().Format(time.RFC3339), // Standard time format + AculManifestVersion: manifest.Metadata.Version, + } + + b, err := json.MarshalIndent(config, "", " ") + if err != nil { + panic(err) // or handle gracefully + } + + // Build full path to acul_config.json inside destDir + configPath := filepath.Join(destDir, "acul_config.json") + + err = os.WriteFile(configPath, b, 0644) + if err != nil { + fmt.Printf("Failed to write config: %v\n", err) + } + fmt.Println("\nProject successfully created!\n" + "Explore the sample app: https://github.com/auth0/acul-sample-app") @@ -263,3 +284,10 @@ func createScreenMap(screens []Screen) map[string]Screen { return screenMap } + +type AculConfig struct { + ChosenTemplate string `json:"chosen_template"` + Screen []string `json:"screens"` // if you want to track this + InitTimestamp string `json:"init_timestamp"` // ISO8601 for readability + AculManifestVersion string `json:"acul_manifest_version"` +} From bddbe339c44ec8d2263a7eabc12557067fdc8fd1 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 8 Sep 2025 10:36:50 +0530 Subject: [PATCH 13/58] Initial commit --- internal/cli/acul.go | 1 + internal/cli/acul_sca.go | 51 ++++++++++- internal/cli/acul_screen_scaffolding.go | 107 ++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 internal/cli/acul_screen_scaffolding.go diff --git a/internal/cli/acul.go b/internal/cli/acul.go index c91902974..60a774c77 100644 --- a/internal/cli/acul.go +++ b/internal/cli/acul.go @@ -13,6 +13,7 @@ func aculCmd(cli *cli) *cobra.Command { // Check out the ./acul_scaffolding_app.MD file for more information on the commands below. cmd.AddCommand(aculInitCmd1(cli)) cmd.AddCommand(aculInitCmd2(cli)) + cmd.AddCommand(aculAddScreenCmd(cli)) return cmd } diff --git a/internal/cli/acul_sca.go b/internal/cli/acul_sca.go index 5f00b2d46..ecc46821d 100644 --- a/internal/cli/acul_sca.go +++ b/internal/cli/acul_sca.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "sync" "time" "github.com/spf13/cobra" @@ -16,6 +17,45 @@ import ( "github.com/auth0/auth0-cli/internal/utils" ) +var ( + manifestLoaded Manifest // type Manifest should match your manifest schema + aculConfigLoaded AculConfig // type AculConfig should match your config schema + manifestOnce sync.Once + aculConfigOnce sync.Once +) + +// LoadManifest Loads manifest.json once +func LoadManifest() (*Manifest, error) { + url := "https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/monorepo-sample/manifest.json" + var manifestErr error + manifestOnce.Do(func() { + resp, err := http.Get(url) + if err != nil { + manifestErr = fmt.Errorf("cannot fetch manifest: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + manifestErr = fmt.Errorf("failed to fetch manifest: received status code %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + manifestErr = fmt.Errorf("cannot read manifest body: %w", err) + } + + if err := json.Unmarshal(body, &manifestLoaded); err != nil { + manifestErr = fmt.Errorf("invalid manifest format: %w", err) + } + }) + + if manifestErr != nil { + return nil, manifestErr + } + + return &manifestLoaded, nil +} + var templateFlag = Flag{ Name: "Template", LongForm: "template", @@ -39,7 +79,7 @@ func aculInitCmd2(_ *cli) *cobra.Command { func runScaffold2(cmd *cobra.Command, args []string) error { // Step 1: fetch manifest.json. - manifest, err := fetchManifest() + manifest, err := LoadManifest() if err != nil { return err } @@ -188,8 +228,13 @@ func runScaffold2(cmd *cobra.Command, args []string) error { fmt.Printf("Failed to write config: %v\n", err) } - fmt.Println("\nProject successfully created!\n" + - "Explore the sample app: https://github.com/auth0/acul-sample-app") + fmt.Println("\nProject successfully created!") + + for _, scr := range selectedScreens { + fmt.Printf("https://auth0.com/docs/acul/screens/+%s\n", scr) + } + + fmt.Println("Explore the sample app: https://github.com/auth0/acul-sample-app") return nil } diff --git a/internal/cli/acul_screen_scaffolding.go b/internal/cli/acul_screen_scaffolding.go new file mode 100644 index 000000000..7064d8fef --- /dev/null +++ b/internal/cli/acul_screen_scaffolding.go @@ -0,0 +1,107 @@ +package cli + +import ( + "encoding/json" + "github.com/auth0/auth0-cli/internal/prompt" + "github.com/auth0/auth0-cli/internal/utils" + "github.com/spf13/cobra" + "io/ioutil" + "os" + "path/filepath" +) + +var destDirFlag = Flag{ + Name: "Destination Directory", + LongForm: "dir", + ShortForm: "d", + Help: "Path to existing project directory (must contain `acul_config.json`)", + IsRequired: false, +} + +func aculAddScreenCmd(_ *cli) *cobra.Command { + var destDir string + cmd := &cobra.Command{ + Use: "add-screen", + Short: "Add screens to an existing project", + Long: `Add screens to an existing project.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runScaffoldAddScreen(cmd, args, destDir) + }, + } + + destDirFlag.RegisterString(cmd, &destDir, ".") + + return cmd +} + +func runScaffoldAddScreen(cmd *cobra.Command, args []string, destDir string) error { + // Step 1: fetch manifest.json. + manifest, err := LoadManifest() + if err != nil { + return err + } + + // Step 2: read acul_config.json from destDir. + aculConfig, err := LoadAculConfig(destDir) + if err != nil { + return err + } + + // Step 2: select screens. + var selectedScreens []string + + if len(args) != 0 { + selectedScreens = args + } else { + var screenOptions []string + + for _, s := range manifest.Templates[aculConfig.ChosenTemplate].Screens { + screenOptions = append(screenOptions, s.ID) + } + + if err = prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...); err != nil { + return err + } + } + + // Step 3: Add screens to existing project. + if err = addScreensToProject(destDir, aculConfig.ChosenTemplate, selectedScreens); err != nil { + return err + } + + return nil +} + +func addScreensToProject(destDir, chosenTemplate string, selectedScreens []string) error { + // --- Step 1: Download and Unzip to Temp Dir ---. + repoURL := "https://github.com/auth0-samples/auth0-acul-samples/archive/refs/heads/monorepo-sample.zip" + tempZipFile := downloadFile(repoURL) + defer os.Remove(tempZipFile) // Clean up the temp zip file. + + tempUnzipDir, err := os.MkdirTemp("", "unzipped-repo-*") + check(err, "Error creating temporary unzipped directory") + defer os.RemoveAll(tempUnzipDir) // Clean up the entire temp directory. + + err = utils.Unzip(tempZipFile, tempUnzipDir) + if err != nil { + return err + } + + return nil + +} + +// Loads acul_config.json once +func LoadAculConfig(destDir string) (*AculConfig, error) { + configPath := filepath.Join(destDir, "acul_config.json") + data, err := ioutil.ReadFile(configPath) + if err != nil { + return nil, err + } + var config AculConfig + err = json.Unmarshal(data, &config) + if err != nil { + return nil, err + } + return &config, nil +} From 2dcda652c71dbda2ea88ac2f3dbeadfcc57ca59e Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Thu, 11 Sep 2025 20:56:01 +0530 Subject: [PATCH 14/58] Add support for add-screen command --- internal/cli/acul_sca.go | 4 +- internal/cli/acul_screen_scaffolding.go | 330 +++++++++++++++++++++++- 2 files changed, 318 insertions(+), 16 deletions(-) diff --git a/internal/cli/acul_sca.go b/internal/cli/acul_sca.go index ecc46821d..0f25c6e2e 100644 --- a/internal/cli/acul_sca.go +++ b/internal/cli/acul_sca.go @@ -231,7 +231,7 @@ func runScaffold2(cmd *cobra.Command, args []string) error { fmt.Println("\nProject successfully created!") for _, scr := range selectedScreens { - fmt.Printf("https://auth0.com/docs/acul/screens/+%s\n", scr) + fmt.Printf("https://auth0.com/docs/acul/screens/%s\n", scr) } fmt.Println("Explore the sample app: https://github.com/auth0/acul-sample-app") @@ -251,7 +251,7 @@ func downloadFile(url string) string { tempFile, err := os.CreateTemp("", "github-zip-*.zip") check(err, "Error creating temporary file") - fmt.Printf("Downloading from %s...\n", url) + // fmt.Printf("Downloading from %s...\n", url) resp, err := http.Get(url) check(err, "Error downloading file") defer resp.Body.Close() diff --git a/internal/cli/acul_screen_scaffolding.go b/internal/cli/acul_screen_scaffolding.go index 7064d8fef..fdded0920 100644 --- a/internal/cli/acul_screen_scaffolding.go +++ b/internal/cli/acul_screen_scaffolding.go @@ -1,13 +1,19 @@ package cli import ( + "crypto/sha256" "encoding/json" - "github.com/auth0/auth0-cli/internal/prompt" - "github.com/auth0/auth0-cli/internal/utils" - "github.com/spf13/cobra" - "io/ioutil" + "fmt" + "io" + "io/fs" + "log" "os" "path/filepath" + + "github.com/spf13/cobra" + + "github.com/auth0/auth0-cli/internal/prompt" + "github.com/auth0/auth0-cli/internal/utils" ) var destDirFlag = Flag{ @@ -25,11 +31,26 @@ func aculAddScreenCmd(_ *cli) *cobra.Command { Short: "Add screens to an existing project", Long: `Add screens to an existing project.`, RunE: func(cmd *cobra.Command, args []string) error { + // Get current working directory + pwd, err := os.Getwd() + if err != nil { + log.Fatalf("Failed to get current directory: %v", err) + } + + if len(destDir) < 1 { + err = destDirFlag.Ask(cmd, &destDir, &pwd) + if err != nil { + return err + } + } else { + destDir = args[0] + } + return runScaffoldAddScreen(cmd, args, destDir) }, } - destDirFlag.RegisterString(cmd, &destDir, ".") + destDirFlag.RegisterString(cmd, &destDir, "") return cmd } @@ -42,7 +63,7 @@ func runScaffoldAddScreen(cmd *cobra.Command, args []string, destDir string) err } // Step 2: read acul_config.json from destDir. - aculConfig, err := LoadAculConfig(destDir) + aculConfig, err := LoadAculConfig(filepath.Join(destDir, "acul_config.json")) if err != nil { return err } @@ -87,21 +108,302 @@ func addScreensToProject(destDir, chosenTemplate string, selectedScreens []strin return err } + // TODO: Adjust this prefix based on the actual structure of the unzipped content(once main branch is used). + var sourcePathPrefix = "auth0-acul-samples-monorepo-sample/" + chosenTemplate + var sourceRoot = filepath.Join(tempUnzipDir, sourcePathPrefix) + + var destRoot = destDir + + missingFiles, _, editedFiles, err := processFiles(manifestLoaded.Templates[chosenTemplate].BaseFiles, sourceRoot, destRoot, chosenTemplate) + if err != nil { + log.Printf("Error processing base files: %v", err) + } + + missingDirFiles, _, editedDirFiles, err := processDirectories(manifestLoaded.Templates[chosenTemplate].BaseDirectories, sourceRoot, destRoot, chosenTemplate) + if err != nil { + log.Printf("Error processing base directories: %v", err) + } + + allEdited := append(editedFiles, editedDirFiles...) + allMissing := append(missingFiles, missingDirFiles...) + + if len(allEdited) > 0 { + fmt.Printf("The following files/directories have been edited and may be overwritten:\n") + for _, p := range allEdited { + fmt.Println(" ", p) + } + + // Show disclaimer before asking for confirmation + fmt.Println("⚠️ DISCLAIMER: Some required base files and directories have been edited.\n" + + "Your added screen(s) may NOT work correctly without these updates.\n" + + "Proceeding without overwriting could lead to inconsistent or unstable behavior.") + + // Now ask for confirmation + if confirmed := prompt.Confirm("Proceed with overwrite and backup? (y/N): "); !confirmed { + fmt.Println("Operation aborted. No files were changed.") + // Handle abort scenario here (return, exit, etc.) + } else { + err = backupAndOverwrite(allEdited, sourceRoot, destRoot) + if err != nil { + fmt.Printf("Backup and overwrite operation finished with errors: %v\n", err) + } else { + fmt.Println("All edited files have been backed up and overwritten successfully.") + } + } + } + + fmt.Println("all missing files:", allMissing) + if len(allMissing) > 0 { + for _, baseFile := range allMissing { + // TODO: Remove hardcoding of removing the template - instead ensure to remove the template name in sourcePathPrefix. + //relPath, err := filepath.Rel(chosenTemplate, baseFile) + //if err != nil { + // continue + //} + + srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, baseFile) + destPath := filepath.Join(destDir, baseFile) + + if _, err = os.Stat(srcPath); os.IsNotExist(err) { + log.Printf("Warning: Source file does not exist: %s", srcPath) + continue + } + + parentDir := filepath.Dir(destPath) + if err := os.MkdirAll(parentDir, 0755); err != nil { + log.Printf("Error creating parent directory for %s: %v", baseFile, err) + continue + } + + err = copyFile(srcPath, destPath) + check(err, fmt.Sprintf("Error copying file %s", baseFile)) + } + // Copy missing files and directories + } + + screenInfo := createScreenMap(manifestLoaded.Templates[chosenTemplate].Screens) + for _, s := range selectedScreens { + screen := screenInfo[s] + + relPath, err := filepath.Rel(chosenTemplate, screen.Path) + if err != nil { + continue + } + + srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, relPath) + destPath := filepath.Join(destDir, relPath) + + if _, err = os.Stat(srcPath); os.IsNotExist(err) { + log.Printf("Warning: Source directory does not exist: %s", srcPath) + continue + } + + parentDir := filepath.Dir(destPath) + if err := os.MkdirAll(parentDir, 0755); err != nil { + log.Printf("Error creating parent directory for %s: %v", screen.Path, err) + continue + } + + fmt.Printf("Copying screen path: %s\n", screen.Path) + err = copyDir(srcPath, destPath) + check(err, fmt.Sprintf("Error copying screen file %s", screen.Path)) + } + return nil +} + +// backupAndOverwrite backs up edited files, then overwrites them with source files +func backupAndOverwrite(allEdited []string, sourceRoot, destRoot string) error { + backupRoot := filepath.Join(destRoot, "back_up") + + // Create back_up directory if it doesn't exist + if err := os.MkdirAll(backupRoot, 0755); err != nil { + return fmt.Errorf("failed to create backup directory: %w", err) + } + + for _, relPath := range allEdited { + destFile := filepath.Join(destRoot, relPath) + backupFile := filepath.Join(backupRoot, relPath) + sourceFile := filepath.Join(sourceRoot, relPath) + + // Backup only if file exists in destination + // Ensure backup directory exists + if err := os.MkdirAll(filepath.Dir(backupFile), 0755); err != nil { + fmt.Printf("Warning: failed to create backup dir for %s: %v\n", relPath, err) + continue + } + // copyFile overwrites backupFile if it exists + if err := copyFile(destFile, backupFile); err != nil { + fmt.Printf("Warning: failed to backup file %s: %v\n", relPath, err) + continue + } + fmt.Printf("Backed up: %s\n", relPath) + // Overwrite destination with source file + if err := copyFile(sourceFile, destFile); err != nil { + fmt.Printf("Error overwriting file %s: %v\n", relPath, err) + continue + } + fmt.Printf("Overwritten: %s\n", relPath) + } + return nil } -// Loads acul_config.json once -func LoadAculConfig(destDir string) (*AculConfig, error) { - configPath := filepath.Join(destDir, "acul_config.json") - data, err := ioutil.ReadFile(configPath) +// processDirectories processes files in all base directories relative to chosenTemplate, +// returning slices of missing, identical, and edited relative file paths. +func processDirectories(baseDirs []string, sourceRoot, destRoot, chosenTemplate string) (missing, identical, edited []string, err error) { + for _, dir := range baseDirs { + // Remove chosenTemplate prefix from dir to get relative base directory + baseDir, relErr := filepath.Rel(chosenTemplate, dir) + if relErr != nil { + return nil, nil, nil, relErr + } + + sourceDir := filepath.Join(sourceRoot, baseDir) + files, listErr := listFilesInDir(sourceDir) + if listErr != nil { + return nil, nil, nil, listErr + } + + for _, sourceFile := range files { + relPath, relErr := filepath.Rel(sourceRoot, sourceFile) + if relErr != nil { + return nil, nil, nil, relErr + } + + destFile := filepath.Join(destRoot, relPath) + editedFlag, compErr := isFileEdited(sourceFile, destFile) + switch { + case compErr != nil && os.IsNotExist(compErr): + missing = append(missing, relPath) + case compErr != nil: + return nil, nil, nil, compErr + case editedFlag: + edited = append(edited, relPath) + default: + identical = append(identical, relPath) + } + } + } + return missing, identical, edited, nil +} + +// Get all files in a directory recursively (for base_directories) +func listFilesInDir(dir string) ([]string, error) { + var files []string + err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + files = append(files, path) + } + return nil + }) + return files, err +} + +func processFiles(baseFiles []string, sourceRoot, destRoot, chosenTemplate string) (missing, identical, edited []string, err error) { + for _, baseFile := range baseFiles { + // TODO: Remove hardcoding of removing the template - instead ensure to remove the template name in sourcePathPrefix. + relPath, err := filepath.Rel(chosenTemplate, baseFile) + if err != nil { + continue + } + + sourcePath := filepath.Join(sourceRoot, relPath) + destPath := filepath.Join(destRoot, relPath) + + editedFlag, err := isFileEdited(sourcePath, destPath) + switch { + case err != nil && os.IsNotExist(err): + missing = append(missing, relPath) + case err != nil: + fmt.Println("Warning: failed to determine if file has been edited:", err) + continue + case editedFlag: + edited = append(edited, relPath) + default: + identical = append(identical, relPath) + } + } + + return +} + +func isFileEdited(source, dest string) (bool, error) { + sourceInfo, err := os.Stat(source) if err != nil { - return nil, err + return false, err } - var config AculConfig - err = json.Unmarshal(data, &config) + + destInfo, err := os.Stat(dest) + if err != nil && os.IsNotExist(err) { + return false, err + } + + if err != nil { + return false, err + } + + if sourceInfo.Size() != destInfo.Size() { + return true, nil + } + // Fallback to hash comparison + hashSource, err := fileHash(source) if err != nil { + return false, err + } + hashDest, err := fileHash(dest) + if err != nil { + return false, err + } + return !equalByteSlices(hashSource, hashDest), nil +} + +func equalByteSlices(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +// Returns SHA256 hash of file at given path +func fileHash(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + h := sha256.New() + // Use buffered copy for performance + if _, err := io.Copy(h, f); err != nil { return nil, err } - return &config, nil + return h.Sum(nil), nil +} + +// LoadAculConfig Loads acul_config.json once +func LoadAculConfig(configPath string) (*AculConfig, error) { + var configErr error + aculConfigOnce.Do(func() { + b, err := os.ReadFile(configPath) + if err != nil { + configErr = err + return + } + err = json.Unmarshal(b, &aculConfigLoaded) + if err != nil { + configErr = err + } + }) + if configErr != nil { + return nil, configErr + } + return &aculConfigLoaded, nil } From 45f4c6da992311c2ac6bcc688415ddd495b16701 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 15 Sep 2025 09:53:51 +0530 Subject: [PATCH 15/58] refactor scaffolding app code --- internal/cli/acul_sca.go | 198 ++++++++++++++++++++++++--------------- 1 file changed, 124 insertions(+), 74 deletions(-) diff --git a/internal/cli/acul_sca.go b/internal/cli/acul_sca.go index 5f00b2d46..22572c0c9 100644 --- a/internal/cli/acul_sca.go +++ b/internal/cli/acul_sca.go @@ -24,76 +24,120 @@ var templateFlag = Flag{ IsRequired: false, } -// This logic goes inside your `RunE` function. -func aculInitCmd2(_ *cli) *cobra.Command { - cmd := &cobra.Command{ +// aculInitCmd2 returns the cobra.Command for project initialization. +func aculInitCmd2(cli *cli) *cobra.Command { + return &cobra.Command{ Use: "init2", Args: cobra.MaximumNArgs(1), Short: "Generate a new project from a template", - Long: `Generate a new project from a template.`, - RunE: runScaffold2, + Long: "Generate a new project from a template.", + RunE: func(cmd *cobra.Command, args []string) error { + return runScaffold2(cli, cmd, args) + }, } - - return cmd } -func runScaffold2(cmd *cobra.Command, args []string) error { - // Step 1: fetch manifest.json. +func runScaffold2(cli *cli, cmd *cobra.Command, args []string) error { manifest, err := fetchManifest() if err != nil { return err } + chosenTemplate, err := selectTemplate(cmd, manifest) + if err != nil { + return err + } + + selectedScreens, err := selectScreens(manifest.Templates[chosenTemplate]) + if err != nil { + return err + } + + destDir := getDestDir(args) + + if err := os.MkdirAll(destDir, 0755); err != nil { + return fmt.Errorf("failed to create project dir: %w", err) + } + + tempUnzipDir, err := downloadAndUnzipSampleRepo() + defer os.RemoveAll(tempUnzipDir) // Clean up the entire temp directory. + if err != nil { + return err + } + + selectedTemplate := manifest.Templates[chosenTemplate] + + err = copyTemplateBaseDirs(cli, selectedTemplate.BaseDirectories, chosenTemplate, tempUnzipDir, destDir) + if err != nil { + return err + } + + err = copyProjectTemplateFiles(cli, selectedTemplate.BaseFiles, chosenTemplate, tempUnzipDir, destDir) + if err != nil { + return err + } + + err = copyProjectScreens(cli, selectedTemplate.Screens, selectedScreens, chosenTemplate, tempUnzipDir, destDir) + if err != nil { + return err + } + + err = writeAculConfig(destDir, chosenTemplate, selectedScreens, manifest.Metadata.Version) + if err != nil { + fmt.Printf("Failed to write config: %v\n", err) + } + + fmt.Println("\nProject successfully created!\n" + + "Explore the sample app: https://github.com/auth0/acul-sample-app") + return nil +} + +func selectTemplate(cmd *cobra.Command, manifest *Manifest) (string, error) { var chosenTemplate string - if err := templateFlag.Select(cmd, &chosenTemplate, utils.FetchKeys(manifest.Templates), nil); err != nil { - return handleInputError(err) + err := templateFlag.Select(cmd, &chosenTemplate, utils.FetchKeys(manifest.Templates), nil) + if err != nil { + return "", handleInputError(err) } + return chosenTemplate, nil +} - // Step 3: select screens. +func selectScreens(template Template) ([]string, error) { var screenOptions []string - template := manifest.Templates[chosenTemplate] for _, s := range template.Screens { screenOptions = append(screenOptions, s.ID) } - - // Step 3: Let user select screens. var selectedScreens []string - if err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...); err != nil { - return err - } + err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...) + return selectedScreens, err +} - // Step 3: Create project folder. - var destDir string +func getDestDir(args []string) string { if len(args) < 1 { - destDir = "my_acul_proj2" - } else { - destDir = args[0] + return "my_acul_proj2" } - if err := os.MkdirAll(destDir, 0755); err != nil { - return fmt.Errorf("failed to create project dir: %w", err) - } - - curr := time.Now() + return args[0] +} - // --- Step 1: Download and Unzip to Temp Dir ---. +func downloadAndUnzipSampleRepo() (string, error) { repoURL := "https://github.com/auth0-samples/auth0-acul-samples/archive/refs/heads/monorepo-sample.zip" tempZipFile := downloadFile(repoURL) defer os.Remove(tempZipFile) // Clean up the temp zip file. tempUnzipDir, err := os.MkdirTemp("", "unzipped-repo-*") - check(err, "Error creating temporary unzipped directory") - defer os.RemoveAll(tempUnzipDir) // Clean up the entire temp directory. - - err = utils.Unzip(tempZipFile, tempUnzipDir) if err != nil { - return err + return "", fmt.Errorf("error creating temporary unzip dir: %w", err) + } + + if err = utils.Unzip(tempZipFile, tempUnzipDir); err != nil { + return "", err } - // TODO: Adjust this prefix based on the actual structure of the unzipped content(once main branch is used). - var sourcePathPrefix = "auth0-acul-samples-monorepo-sample/" + chosenTemplate + return tempUnzipDir, nil +} - // --- Step 2: Copy the Specified Base Directories ---. - for _, dir := range manifest.Templates[chosenTemplate].BaseDirectories { +func copyTemplateBaseDirs(cli *cli, baseDirs []string, chosenTemplate, tempUnzipDir, destDir string) error { + sourcePathPrefix := "auth0-acul-samples-monorepo-sample/" + chosenTemplate + for _, dir := range baseDirs { // TODO: Remove hardcoding of removing the template - instead ensure to remove the template name in sourcePathPrefix. relPath, err := filepath.Rel(chosenTemplate, dir) if err != nil { @@ -104,16 +148,21 @@ func runScaffold2(cmd *cobra.Command, args []string) error { destPath := filepath.Join(destDir, relPath) if _, err = os.Stat(srcPath); os.IsNotExist(err) { - log.Printf("Warning: Source directory does not exist: %s", srcPath) + cli.renderer.Warnf("Warning: Source directory does not exist: %s", srcPath) continue } - err = copyDir(srcPath, destPath) - check(err, fmt.Sprintf("Error copying directory %s", dir)) + if err := copyDir(srcPath, destPath); err != nil { + return fmt.Errorf("error copying directory %s: %w", dir, err) + } } - // --- Step 3: Copy the Specified Base Files ---. - for _, baseFile := range manifest.Templates[chosenTemplate].BaseFiles { + return nil +} + +func copyProjectTemplateFiles(cli *cli, baseFiles []string, chosenTemplate, tempUnzipDir, destDir string) error { + sourcePathPrefix := "auth0-acul-samples-monorepo-sample/" + chosenTemplate + for _, baseFile := range baseFiles { // TODO: Remove hardcoding of removing the template - instead ensure to remove the template name in sourcePathPrefix. relPath, err := filepath.Rel(chosenTemplate, baseFile) if err != nil { @@ -124,21 +173,27 @@ func runScaffold2(cmd *cobra.Command, args []string) error { destPath := filepath.Join(destDir, relPath) if _, err = os.Stat(srcPath); os.IsNotExist(err) { - log.Printf("Warning: Source file does not exist: %s", srcPath) + cli.renderer.Warnf("Warning: Source file does not exist: %s", srcPath) continue } parentDir := filepath.Dir(destPath) if err := os.MkdirAll(parentDir, 0755); err != nil { - log.Printf("Error creating parent directory for %s: %v", baseFile, err) + cli.renderer.Warnf("Error creating parent directory for %s: %v", baseFile, err) continue } - err = copyFile(srcPath, destPath) - check(err, fmt.Sprintf("Error copying file %s", baseFile)) + if err := copyFile(srcPath, destPath); err != nil { + return fmt.Errorf("error copying file %s: %w", baseFile, err) + } } - screenInfo := createScreenMap(template.Screens) + return nil +} + +func copyProjectScreens(cli *cli, screens []Screen, selectedScreens []string, chosenTemplate, tempUnzipDir, destDir string) error { + sourcePathPrefix := "auth0-acul-samples-monorepo-sample/" + chosenTemplate + screenInfo := createScreenMap(screens) for _, s := range selectedScreens { screen := screenInfo[s] @@ -151,57 +206,54 @@ func runScaffold2(cmd *cobra.Command, args []string) error { destPath := filepath.Join(destDir, relPath) if _, err = os.Stat(srcPath); os.IsNotExist(err) { - log.Printf("Warning: Source directory does not exist: %s", srcPath) + cli.renderer.Warnf("Warning: Source directory does not exist: %s", srcPath) continue } parentDir := filepath.Dir(destPath) if err := os.MkdirAll(parentDir, 0755); err != nil { - log.Printf("Error creating parent directory for %s: %v", screen.Path, err) + cli.renderer.Warnf("Error creating parent directory for %s: %v", screen.Path, err) continue } fmt.Printf("Copying screen path: %s\n", screen.Path) - err = copyDir(srcPath, destPath) - check(err, fmt.Sprintf("Error copying screen file %s", screen.Path)) + if err := copyDir(srcPath, destPath); err != nil { + return fmt.Errorf("error copying screen directory %s: %w", screen.Path, err) + } } - fmt.Println(time.Since(curr)) + return nil +} +func writeAculConfig(destDir, chosenTemplate string, selectedScreens []string, manifestVersion string) error { config := AculConfig{ ChosenTemplate: chosenTemplate, - Screen: selectedScreens, // If needed - InitTimestamp: time.Now().Format(time.RFC3339), // Standard time format - AculManifestVersion: manifest.Metadata.Version, + Screen: selectedScreens, + InitTimestamp: time.Now().Format(time.RFC3339), + AculManifestVersion: manifestVersion, } - b, err := json.MarshalIndent(config, "", " ") + data, err := json.MarshalIndent(config, "", " ") if err != nil { - panic(err) // or handle gracefully + return fmt.Errorf("failed to marshal config: %w", err) } - // Build full path to acul_config.json inside destDir configPath := filepath.Join(destDir, "acul_config.json") - - err = os.WriteFile(configPath, b, 0644) - if err != nil { - fmt.Printf("Failed to write config: %v\n", err) + if err = os.WriteFile(configPath, data, 0644); err != nil { + return fmt.Errorf("failed to write config: %v", err) } - fmt.Println("\nProject successfully created!\n" + - "Explore the sample app: https://github.com/auth0/acul-sample-app") - return nil } -// Helper function to handle errors and log them. +// Helper function to handle errors and log them, exiting the process. func check(err error, msg string) { if err != nil { - log.Fatalf("%s: %v", err, msg) + log.Fatalf("%s: %v", msg, err) } } -// Function to download a file from a URL to a temporary location. +// downloadFile downloads a file from a URL to a temporary file and returns its name. func downloadFile(url string) string { tempFile, err := os.CreateTemp("", "github-zip-*.zip") check(err, "Error creating temporary file") @@ -236,8 +288,7 @@ func copyFile(src, dst string) error { } defer out.Close() - _, err = io.Copy(out, in) - if err != nil { + if _, err = io.Copy(out, in); err != nil { return fmt.Errorf("failed to copy file contents: %w", err) } return out.Close() @@ -281,13 +332,12 @@ func createScreenMap(screens []Screen) map[string]Screen { for _, screen := range screens { screenMap[screen.ID] = screen } - return screenMap } type AculConfig struct { ChosenTemplate string `json:"chosen_template"` - Screen []string `json:"screens"` // if you want to track this - InitTimestamp string `json:"init_timestamp"` // ISO8601 for readability + Screen []string `json:"screens"` + InitTimestamp string `json:"init_timestamp"` AculManifestVersion string `json:"acul_manifest_version"` } From 8b54ceea13aedf2f74ae46fa4e0ee41f1cf98c34 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 15 Sep 2025 10:09:55 +0530 Subject: [PATCH 16/58] refactor scaffolding app code --- docs/auth0_acul.md | 2 +- docs/{auth0_acul_init2.md => auth0_acul_init.md} | 6 +++--- docs/auth0_acul_init1.md | 2 +- internal/cli/acul.go | 2 +- internal/cli/acul_sacffolding_app.MD | 4 ++-- internal/cli/acul_sca.go | 10 ++++------ 6 files changed, 12 insertions(+), 14 deletions(-) rename docs/{auth0_acul_init2.md => auth0_acul_init.md} (81%) diff --git a/docs/auth0_acul.md b/docs/auth0_acul.md index b9850667f..270c42e24 100644 --- a/docs/auth0_acul.md +++ b/docs/auth0_acul.md @@ -10,6 +10,6 @@ Customize the Universal Login experience. This requires a custom domain to be co ## Commands - [auth0 acul config](auth0_acul_config.md) - Configure Advanced Customizations for Universal Login screens. +- [auth0 acul init](auth0_acul_init.md) - Generate a new project from a template - [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template -- [auth0 acul init2](auth0_acul_init2.md) - Generate a new project from a template diff --git a/docs/auth0_acul_init2.md b/docs/auth0_acul_init.md similarity index 81% rename from docs/auth0_acul_init2.md rename to docs/auth0_acul_init.md index 0170b9bdf..e6bc89b46 100644 --- a/docs/auth0_acul_init2.md +++ b/docs/auth0_acul_init.md @@ -3,13 +3,13 @@ layout: default parent: auth0 acul has_toc: false --- -# auth0 acul init2 +# auth0 acul init Generate a new project from a template. ## Usage ``` -auth0 acul init2 [flags] +auth0 acul init [flags] ``` ## Examples @@ -34,7 +34,7 @@ auth0 acul init2 [flags] ## Related Commands - [auth0 acul config](auth0_acul_config.md) - Configure Advanced Customizations for Universal Login screens. +- [auth0 acul init](auth0_acul_init.md) - Generate a new project from a template - [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template -- [auth0 acul init2](auth0_acul_init2.md) - Generate a new project from a template diff --git a/docs/auth0_acul_init1.md b/docs/auth0_acul_init1.md index 3b97e62b2..3f5082a58 100644 --- a/docs/auth0_acul_init1.md +++ b/docs/auth0_acul_init1.md @@ -34,7 +34,7 @@ auth0 acul init1 [flags] ## Related Commands - [auth0 acul config](auth0_acul_config.md) - Configure Advanced Customizations for Universal Login screens. +- [auth0 acul init](auth0_acul_init.md) - Generate a new project from a template - [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template -- [auth0 acul init2](auth0_acul_init2.md) - Generate a new project from a template diff --git a/internal/cli/acul.go b/internal/cli/acul.go index 8c0f58e45..87186dd3f 100644 --- a/internal/cli/acul.go +++ b/internal/cli/acul.go @@ -12,7 +12,7 @@ func aculCmd(cli *cli) *cobra.Command { cmd.AddCommand(aculConfigureCmd(cli)) // Check out the ./acul_scaffolding_app.MD file for more information on the commands below. cmd.AddCommand(aculInitCmd1(cli)) - cmd.AddCommand(aculInitCmd2(cli)) + cmd.AddCommand(aculInitCmd(cli)) return cmd } diff --git a/internal/cli/acul_sacffolding_app.MD b/internal/cli/acul_sacffolding_app.MD index c693f17cb..ecb8e3f50 100644 --- a/internal/cli/acul_sacffolding_app.MD +++ b/internal/cli/acul_sacffolding_app.MD @@ -19,7 +19,7 @@ Initializes a git repo in the target directory, enables sparse-checkout, writes --- ## Method B: HTTP Raw + GitHub Tree API -*File: `internal/cli/acul_scaffolding.go` (command `init`)* +*File: `internal/cli/acul_scaffolding.go` (command `init2`)* **Summary:** Uses the GitHub Tree API to enumerate files and `raw.githubusercontent.com` to download each file individually to a target folder. @@ -38,7 +38,7 @@ Uses the GitHub Tree API to enumerate files and `raw.githubusercontent.com` to d --- ## Method C: Zip Download + Selective Copy -*File: `internal/cli/acul_sca.go` (command `init2`)* +*File: `internal/cli/acul_sca.go` (command `init`)* **Summary:** Downloads a branch zip archive once, unzips to a temp directory, then copies only base directories/files and selected screens into the target directory. diff --git a/internal/cli/acul_sca.go b/internal/cli/acul_sca.go index 22572c0c9..bee1a34ae 100644 --- a/internal/cli/acul_sca.go +++ b/internal/cli/acul_sca.go @@ -24,10 +24,10 @@ var templateFlag = Flag{ IsRequired: false, } -// aculInitCmd2 returns the cobra.Command for project initialization. -func aculInitCmd2(cli *cli) *cobra.Command { +// aculInitCmd returns the cobra.Command for project initialization. +func aculInitCmd(cli *cli) *cobra.Command { return &cobra.Command{ - Use: "init2", + Use: "init", Args: cobra.MaximumNArgs(1), Short: "Generate a new project from a template", Long: "Generate a new project from a template.", @@ -113,7 +113,7 @@ func selectScreens(template Template) ([]string, error) { func getDestDir(args []string) string { if len(args) < 1 { - return "my_acul_proj2" + return "my_acul_proj" } return args[0] } @@ -216,7 +216,6 @@ func copyProjectScreens(cli *cli, screens []Screen, selectedScreens []string, ch continue } - fmt.Printf("Copying screen path: %s\n", screen.Path) if err := copyDir(srcPath, destPath); err != nil { return fmt.Errorf("error copying screen directory %s: %w", screen.Path, err) } @@ -258,7 +257,6 @@ func downloadFile(url string) string { tempFile, err := os.CreateTemp("", "github-zip-*.zip") check(err, "Error creating temporary file") - fmt.Printf("Downloading from %s...\n", url) resp, err := http.Get(url) check(err, "Error downloading file") defer resp.Body.Close() From 638461ad8badf16bdf1749455cd3f368f1c2e912 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 15 Sep 2025 19:39:55 +0530 Subject: [PATCH 17/58] refactor screen scaffolding --- docs/auth0_acul.md | 1 + docs/auth0_acul_init.md | 1 + docs/auth0_acul_init1.md | 1 + docs/auth0_acul_screen.md | 13 + docs/auth0_acul_screen_add.md | 43 ++++ internal/cli/acul.go | 14 +- internal/cli/acul_sca.go | 18 +- internal/cli/acul_screen_scaffolding.go | 303 +++++++++++++----------- 8 files changed, 235 insertions(+), 159 deletions(-) create mode 100644 docs/auth0_acul_screen.md create mode 100644 docs/auth0_acul_screen_add.md diff --git a/docs/auth0_acul.md b/docs/auth0_acul.md index 270c42e24..a67842f8e 100644 --- a/docs/auth0_acul.md +++ b/docs/auth0_acul.md @@ -12,4 +12,5 @@ Customize the Universal Login experience. This requires a custom domain to be co - [auth0 acul config](auth0_acul_config.md) - Configure Advanced Customizations for Universal Login screens. - [auth0 acul init](auth0_acul_init.md) - Generate a new project from a template - [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template +- [auth0 acul screen](auth0_acul_screen.md) - Manage individual screens for Advanced Customizations for Universal Login. diff --git a/docs/auth0_acul_init.md b/docs/auth0_acul_init.md index e6bc89b46..c39283f2a 100644 --- a/docs/auth0_acul_init.md +++ b/docs/auth0_acul_init.md @@ -36,5 +36,6 @@ auth0 acul init [flags] - [auth0 acul config](auth0_acul_config.md) - Configure Advanced Customizations for Universal Login screens. - [auth0 acul init](auth0_acul_init.md) - Generate a new project from a template - [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template +- [auth0 acul screen](auth0_acul_screen.md) - Manage individual screens for Advanced Customizations for Universal Login. diff --git a/docs/auth0_acul_init1.md b/docs/auth0_acul_init1.md index 3f5082a58..8819d84de 100644 --- a/docs/auth0_acul_init1.md +++ b/docs/auth0_acul_init1.md @@ -36,5 +36,6 @@ auth0 acul init1 [flags] - [auth0 acul config](auth0_acul_config.md) - Configure Advanced Customizations for Universal Login screens. - [auth0 acul init](auth0_acul_init.md) - Generate a new project from a template - [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template +- [auth0 acul screen](auth0_acul_screen.md) - Manage individual screens for Advanced Customizations for Universal Login. diff --git a/docs/auth0_acul_screen.md b/docs/auth0_acul_screen.md new file mode 100644 index 000000000..8972361ea --- /dev/null +++ b/docs/auth0_acul_screen.md @@ -0,0 +1,13 @@ +--- +layout: default +has_toc: false +has_children: true +--- +# auth0 acul screen + +Manage individual screens for Auth0 Universal Login using ACUL (Advanced Customizations). + +## Commands + +- [auth0 acul screen add](auth0_acul_screen_add.md) - Add screens to an existing project + diff --git a/docs/auth0_acul_screen_add.md b/docs/auth0_acul_screen_add.md new file mode 100644 index 000000000..55002845b --- /dev/null +++ b/docs/auth0_acul_screen_add.md @@ -0,0 +1,43 @@ +--- +layout: default +parent: auth0 acul screen +has_toc: false +--- +# auth0 acul screen add + +Add screens to an existing project. + +## Usage +``` +auth0 acul screen add [flags] +``` + +## Examples + +``` + +``` + + +## Flags + +``` + -d, --dir acul_config.json Path to existing project directory (must contain acul_config.json) +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 acul screen add](auth0_acul_screen_add.md) - Add screens to an existing project + + diff --git a/internal/cli/acul.go b/internal/cli/acul.go index 75f3abfe6..7700c09e4 100644 --- a/internal/cli/acul.go +++ b/internal/cli/acul.go @@ -13,6 +13,19 @@ func aculCmd(cli *cli) *cobra.Command { // Check out the ./acul_scaffolding_app.MD file for more information on the commands below. cmd.AddCommand(aculInitCmd1(cli)) cmd.AddCommand(aculInitCmd(cli)) + cmd.AddCommand(aculScreenCmd(cli)) + + return cmd +} + +func aculScreenCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "screen", + Short: "Manage individual screens for Advanced Customizations for Universal Login.", + Long: "Manage individual screens for Auth0 Universal Login using ACUL (Advanced Customizations).", + } + + cmd.AddCommand(aculScreenAddCmd(cli)) return cmd } @@ -29,7 +42,6 @@ func aculConfigureCmd(cli *cli) *cobra.Command { cmd.AddCommand(aculConfigSetCmd(cli)) cmd.AddCommand(aculConfigListCmd(cli)) cmd.AddCommand(aculConfigDocsCmd(cli)) - cmd.AddCommand(aculAddScreenCmd(cli)) return cmd } diff --git a/internal/cli/acul_sca.go b/internal/cli/acul_sca.go index 025cbb7bc..c7f3d4261 100644 --- a/internal/cli/acul_sca.go +++ b/internal/cli/acul_sca.go @@ -18,10 +18,8 @@ import ( ) var ( - manifestLoaded Manifest // type Manifest should match your manifest schema - aculConfigLoaded AculConfig // type AculConfig should match your config schema - manifestOnce sync.Once - aculConfigOnce sync.Once + manifestLoaded Manifest // type Manifest should match your manifest schema + manifestOnce sync.Once ) // LoadManifest Loads manifest.json once @@ -267,7 +265,7 @@ func copyProjectScreens(cli *cli, screens []Screen, selectedScreens []string, ch func writeAculConfig(destDir, chosenTemplate string, selectedScreens []string, manifestVersion string) error { config := AculConfig{ ChosenTemplate: chosenTemplate, - Screen: selectedScreens, + Screens: selectedScreens, InitTimestamp: time.Now().Format(time.RFC3339), AculManifestVersion: manifestVersion, } @@ -282,14 +280,6 @@ func writeAculConfig(destDir, chosenTemplate string, selectedScreens []string, m return fmt.Errorf("failed to write config: %v", err) } - fmt.Println("\nProject successfully created!") - - for _, scr := range selectedScreens { - fmt.Printf("https://auth0.com/docs/acul/screens/%s\n", scr) - } - - fmt.Println("Explore the sample app: https://github.com/auth0/acul-sample-app") - return nil } @@ -383,7 +373,7 @@ func createScreenMap(screens []Screen) map[string]Screen { type AculConfig struct { ChosenTemplate string `json:"chosen_template"` - Screen []string `json:"screens"` + Screens []string `json:"screens"` InitTimestamp string `json:"init_timestamp"` AculManifestVersion string `json:"acul_manifest_version"` } diff --git a/internal/cli/acul_screen_scaffolding.go b/internal/cli/acul_screen_scaffolding.go index fdded0920..ed5e8f850 100644 --- a/internal/cli/acul_screen_scaffolding.go +++ b/internal/cli/acul_screen_scaffolding.go @@ -9,11 +9,13 @@ import ( "log" "os" "path/filepath" + "sync" + + "github.com/auth0/auth0-cli/internal/ansi" "github.com/spf13/cobra" "github.com/auth0/auth0-cli/internal/prompt" - "github.com/auth0/auth0-cli/internal/utils" ) var destDirFlag = Flag{ @@ -24,17 +26,21 @@ var destDirFlag = Flag{ IsRequired: false, } -func aculAddScreenCmd(_ *cli) *cobra.Command { +var ( + aculConfigOnce sync.Once + aculConfigLoaded AculConfig +) + +func aculScreenAddCmd(cli *cli) *cobra.Command { var destDir string cmd := &cobra.Command{ - Use: "add-screen", + Use: "add", Short: "Add screens to an existing project", - Long: `Add screens to an existing project.`, + Long: "Add screens to an existing project.", RunE: func(cmd *cobra.Command, args []string) error { - // Get current working directory pwd, err := os.Getwd() if err != nil { - log.Fatalf("Failed to get current directory: %v", err) + return fmt.Errorf("failed to get current directory: %v", err) } if len(destDir) < 1 { @@ -42,11 +48,9 @@ func aculAddScreenCmd(_ *cli) *cobra.Command { if err != nil { return err } - } else { - destDir = args[0] } - return runScaffoldAddScreen(cmd, args, destDir) + return scaffoldAddScreen(cli, args, destDir) }, } @@ -55,220 +59,236 @@ func aculAddScreenCmd(_ *cli) *cobra.Command { return cmd } -func runScaffoldAddScreen(cmd *cobra.Command, args []string, destDir string) error { - // Step 1: fetch manifest.json. +func scaffoldAddScreen(cli *cli, args []string, destDir string) error { manifest, err := LoadManifest() if err != nil { return err } - // Step 2: read acul_config.json from destDir. aculConfig, err := LoadAculConfig(filepath.Join(destDir, "acul_config.json")) if err != nil { return err } - // Step 2: select screens. - var selectedScreens []string + selectedScreens, err := chooseScreens(args, manifest, aculConfig.ChosenTemplate) + if err != nil { + return err + } - if len(args) != 0 { - selectedScreens = args - } else { - var screenOptions []string + selectedScreens = filterScreensForOverwrite(selectedScreens, aculConfig.Screens) - for _, s := range manifest.Templates[aculConfig.ChosenTemplate].Screens { - screenOptions = append(screenOptions, s.ID) - } + if err = addScreensToProject(cli, destDir, aculConfig.ChosenTemplate, selectedScreens, manifest.Templates[aculConfig.ChosenTemplate]); err != nil { + return err + } - if err = prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...); err != nil { - return err - } + // Update acul_config.json with new screens. + aculConfig.Screens = append(aculConfig.Screens, selectedScreens...) + configBytes, err := json.MarshalIndent(aculConfig, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal updated acul_config.json: %w", err) } - // Step 3: Add screens to existing project. - if err = addScreensToProject(destDir, aculConfig.ChosenTemplate, selectedScreens); err != nil { - return err + if err := os.WriteFile(filepath.Join(destDir, "acul_config.json"), configBytes, 0644); err != nil { + return fmt.Errorf("failed to write updated acul_config.json: %w", err) } + cli.renderer.Infof(ansi.Bold(ansi.Green("Screens added successfully"))) + return nil } -func addScreensToProject(destDir, chosenTemplate string, selectedScreens []string) error { - // --- Step 1: Download and Unzip to Temp Dir ---. - repoURL := "https://github.com/auth0-samples/auth0-acul-samples/archive/refs/heads/monorepo-sample.zip" - tempZipFile := downloadFile(repoURL) - defer os.Remove(tempZipFile) // Clean up the temp zip file. +// Filter out screens user does not want to overwrite. +func filterScreensForOverwrite(selectedScreens []string, existingScreens []string) []string { + var finalScreens []string + for _, s := range selectedScreens { + if screenExists(existingScreens, s) { + promptMsg := fmt.Sprintf("Screen '%s' already exists. Do you want to overwrite its directory? (y/N): ", s) + if !prompt.Confirm(promptMsg) { + continue + } + } + finalScreens = append(finalScreens, s) + } + return finalScreens +} - tempUnzipDir, err := os.MkdirTemp("", "unzipped-repo-*") - check(err, "Error creating temporary unzipped directory") - defer os.RemoveAll(tempUnzipDir) // Clean up the entire temp directory. +// Helper to check if a screen exists in the slice. +func screenExists(screens []string, target string) bool { + for _, screen := range screens { + if screen == target { + return true + } + } + return false +} + +// Select screens: from args or prompt. +func chooseScreens(args []string, manifest *Manifest, chosenTemplate string) ([]string, error) { + if len(args) != 0 { + return args, nil + } + + selectedScreens, err := selectScreens(manifest.Templates[chosenTemplate]) + if err != nil { + return nil, err + } - err = utils.Unzip(tempZipFile, tempUnzipDir) + return selectedScreens, nil +} + +func addScreensToProject(cli *cli, destDir, chosenTemplate string, selectedScreens []string, selectedTemplate Template) error { + tempUnzipDir, err := downloadAndUnzipSampleRepo() + defer os.RemoveAll(tempUnzipDir) // Clean up the entire temp directory. if err != nil { return err } // TODO: Adjust this prefix based on the actual structure of the unzipped content(once main branch is used). - var sourcePathPrefix = "auth0-acul-samples-monorepo-sample/" + chosenTemplate - var sourceRoot = filepath.Join(tempUnzipDir, sourcePathPrefix) - + var sourcePrefix = "auth0-acul-samples-monorepo-sample/" + chosenTemplate + var sourceRoot = filepath.Join(tempUnzipDir, sourcePrefix) var destRoot = destDir - missingFiles, _, editedFiles, err := processFiles(manifestLoaded.Templates[chosenTemplate].BaseFiles, sourceRoot, destRoot, chosenTemplate) + missingFiles, editedFiles, err := processFiles(cli, selectedTemplate.BaseFiles, sourceRoot, destRoot, chosenTemplate) if err != nil { log.Printf("Error processing base files: %v", err) } - missingDirFiles, _, editedDirFiles, err := processDirectories(manifestLoaded.Templates[chosenTemplate].BaseDirectories, sourceRoot, destRoot, chosenTemplate) + missingDirFiles, editedDirFiles, err := processDirectories(cli, selectedTemplate.BaseDirectories, sourceRoot, destRoot, chosenTemplate) if err != nil { log.Printf("Error processing base directories: %v", err) } - allEdited := append(editedFiles, editedDirFiles...) - allMissing := append(missingFiles, missingDirFiles...) + editedFiles = append(editedFiles, editedDirFiles...) + missingFiles = append(missingFiles, missingDirFiles...) - if len(allEdited) > 0 { - fmt.Printf("The following files/directories have been edited and may be overwritten:\n") - for _, p := range allEdited { - fmt.Println(" ", p) - } + err = handleEditedFiles(cli, editedFiles, sourceRoot, destRoot) + if err != nil { + return fmt.Errorf("error during backup/overwrite: %w", err) + } - // Show disclaimer before asking for confirmation - fmt.Println("⚠️ DISCLAIMER: Some required base files and directories have been edited.\n" + - "Your added screen(s) may NOT work correctly without these updates.\n" + - "Proceeding without overwriting could lead to inconsistent or unstable behavior.") - - // Now ask for confirmation - if confirmed := prompt.Confirm("Proceed with overwrite and backup? (y/N): "); !confirmed { - fmt.Println("Operation aborted. No files were changed.") - // Handle abort scenario here (return, exit, etc.) - } else { - err = backupAndOverwrite(allEdited, sourceRoot, destRoot) - if err != nil { - fmt.Printf("Backup and overwrite operation finished with errors: %v\n", err) - } else { - fmt.Println("All edited files have been backed up and overwritten successfully.") - } - } + err = handleMissingFiles(cli, missingFiles, tempUnzipDir, sourcePrefix, destDir) + if err != nil { + return fmt.Errorf("error copying missing files: %w", err) } - fmt.Println("all missing files:", allMissing) - if len(allMissing) > 0 { - for _, baseFile := range allMissing { - // TODO: Remove hardcoding of removing the template - instead ensure to remove the template name in sourcePathPrefix. - //relPath, err := filepath.Rel(chosenTemplate, baseFile) - //if err != nil { - // continue - //} + return copyProjectScreens(cli, selectedTemplate.Screens, selectedScreens, chosenTemplate, tempUnzipDir, destDir) +} - srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, baseFile) - destPath := filepath.Join(destDir, baseFile) +func handleEditedFiles(cli *cli, edited []string, sourceRoot, destRoot string) error { + if len(edited) < 1 { + return nil + } - if _, err = os.Stat(srcPath); os.IsNotExist(err) { - log.Printf("Warning: Source file does not exist: %s", srcPath) - continue - } + fmt.Println("Edited files/directories may be overwritten:") + for _, p := range edited { + fmt.Println(" ", p) + } - parentDir := filepath.Dir(destPath) - if err := os.MkdirAll(parentDir, 0755); err != nil { - log.Printf("Error creating parent directory for %s: %v", baseFile, err) - continue - } + fmt.Println("⚠️ DISCLAIMER: Some required base files and directories have been edited.\n" + + "Your added screen(s) may NOT work correctly without these updates.\n" + + "Proceeding without overwriting could lead to inconsistent or unstable behavior.") - err = copyFile(srcPath, destPath) - check(err, fmt.Sprintf("Error copying file %s", baseFile)) - } - // Copy missing files and directories + if !prompt.Confirm("Proceed with overwrite and backup? (y/N): ") { + cli.renderer.Warnf("User opted not to overwrite modified files.") + return nil } - screenInfo := createScreenMap(manifestLoaded.Templates[chosenTemplate].Screens) - for _, s := range selectedScreens { - screen := screenInfo[s] + err := backupAndOverwrite(cli, edited, sourceRoot, destRoot) + if err != nil { + cli.renderer.Warnf("Error during backup and overwrite: %v\n", err) + return err + } - relPath, err := filepath.Rel(chosenTemplate, screen.Path) - if err != nil { - continue - } + cli.renderer.Infof(ansi.Bold(ansi.Blue("Edited files backed up to back_up folder and overwritten."))) - srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, relPath) - destPath := filepath.Join(destDir, relPath) + return nil +} - if _, err = os.Stat(srcPath); os.IsNotExist(err) { - log.Printf("Warning: Source directory does not exist: %s", srcPath) - continue - } +// Copy missing files from source to destination. +func handleMissingFiles(cli *cli, missing []string, tempUnzipDir, sourcePrefix, destDir string) error { + if len(missing) > 0 { + for _, baseFile := range missing { + srcPath := filepath.Join(tempUnzipDir, sourcePrefix, baseFile) + destPath := filepath.Join(destDir, baseFile) + if _, err := os.Stat(srcPath); os.IsNotExist(err) { + cli.renderer.Warnf("Warning: Source file does not exist: %s", srcPath) + continue + } - parentDir := filepath.Dir(destPath) - if err := os.MkdirAll(parentDir, 0755); err != nil { - log.Printf("Error creating parent directory for %s: %v", screen.Path, err) - continue - } + parentDir := filepath.Dir(destPath) + if err := os.MkdirAll(parentDir, 0755); err != nil { + cli.renderer.Warnf("Error creating parent dir for %s: %v", baseFile, err) + continue + } - fmt.Printf("Copying screen path: %s\n", screen.Path) - err = copyDir(srcPath, destPath) - check(err, fmt.Sprintf("Error copying screen file %s", screen.Path)) + if err := copyFile(srcPath, destPath); err != nil { + return fmt.Errorf("error copying file %s: %w", baseFile, err) + } + } } - return nil } -// backupAndOverwrite backs up edited files, then overwrites them with source files -func backupAndOverwrite(allEdited []string, sourceRoot, destRoot string) error { +// backupAndOverwrite backs up edited files, then overwrites them with source files. +func backupAndOverwrite(cli *cli, edited []string, sourceRoot, destRoot string) error { backupRoot := filepath.Join(destRoot, "back_up") - // Create back_up directory if it doesn't exist + // Remove existing backup folder if it exists. + if _, err := os.Stat(backupRoot); err == nil { + if err := os.RemoveAll(backupRoot); err != nil { + return fmt.Errorf("failed to clear existing backup folder: %w", err) + } + } + + // Create a fresh backup folder. if err := os.MkdirAll(backupRoot, 0755); err != nil { return fmt.Errorf("failed to create backup directory: %w", err) } - for _, relPath := range allEdited { + for _, relPath := range edited { destFile := filepath.Join(destRoot, relPath) backupFile := filepath.Join(backupRoot, relPath) sourceFile := filepath.Join(sourceRoot, relPath) - // Backup only if file exists in destination - // Ensure backup directory exists if err := os.MkdirAll(filepath.Dir(backupFile), 0755); err != nil { - fmt.Printf("Warning: failed to create backup dir for %s: %v\n", relPath, err) + cli.renderer.Warnf("Failed to create backup directory for %s: %v", relPath, err) continue } - // copyFile overwrites backupFile if it exists + if err := copyFile(destFile, backupFile); err != nil { - fmt.Printf("Warning: failed to backup file %s: %v\n", relPath, err) + cli.renderer.Warnf("Failed to backup file %s: %v", relPath, err) continue } - fmt.Printf("Backed up: %s\n", relPath) - // Overwrite destination with source file if err := copyFile(sourceFile, destFile); err != nil { - fmt.Printf("Error overwriting file %s: %v\n", relPath, err) + cli.renderer.Errorf("Failed to overwrite file %s: %v", relPath, err) continue } - fmt.Printf("Overwritten: %s\n", relPath) + + cli.renderer.Infof("Overwritten: %s", relPath) } return nil } -// processDirectories processes files in all base directories relative to chosenTemplate, -// returning slices of missing, identical, and edited relative file paths. -func processDirectories(baseDirs []string, sourceRoot, destRoot, chosenTemplate string) (missing, identical, edited []string, err error) { +// processDirectories processes files in all base directories relative to chosenTemplate. +func processDirectories(cli *cli, baseDirs []string, sourceRoot, destRoot, chosenTemplate string) (missing, edited []string, err error) { for _, dir := range baseDirs { - // Remove chosenTemplate prefix from dir to get relative base directory + // TODO: Remove chosenTemplate prefix from dir to get relative base directory. baseDir, relErr := filepath.Rel(chosenTemplate, dir) if relErr != nil { - return nil, nil, nil, relErr + return } sourceDir := filepath.Join(sourceRoot, baseDir) files, listErr := listFilesInDir(sourceDir) if listErr != nil { - return nil, nil, nil, listErr + return } for _, sourceFile := range files { relPath, relErr := filepath.Rel(sourceRoot, sourceFile) if relErr != nil { - return nil, nil, nil, relErr + continue } destFile := filepath.Join(destRoot, relPath) @@ -277,18 +297,16 @@ func processDirectories(baseDirs []string, sourceRoot, destRoot, chosenTemplate case compErr != nil && os.IsNotExist(compErr): missing = append(missing, relPath) case compErr != nil: - return nil, nil, nil, compErr + cli.renderer.Warnf("Warning: failed to determine if file has been edited: %v", compErr) + continue case editedFlag: edited = append(edited, relPath) - default: - identical = append(identical, relPath) } } } - return missing, identical, edited, nil + return } -// Get all files in a directory recursively (for base_directories) func listFilesInDir(dir string) ([]string, error) { var files []string err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { @@ -300,10 +318,11 @@ func listFilesInDir(dir string) ([]string, error) { } return nil }) + return files, err } -func processFiles(baseFiles []string, sourceRoot, destRoot, chosenTemplate string) (missing, identical, edited []string, err error) { +func processFiles(cli *cli, baseFiles []string, sourceRoot, destRoot, chosenTemplate string) (missing, edited []string, err error) { for _, baseFile := range baseFiles { // TODO: Remove hardcoding of removing the template - instead ensure to remove the template name in sourcePathPrefix. relPath, err := filepath.Rel(chosenTemplate, baseFile) @@ -319,15 +338,12 @@ func processFiles(baseFiles []string, sourceRoot, destRoot, chosenTemplate strin case err != nil && os.IsNotExist(err): missing = append(missing, relPath) case err != nil: - fmt.Println("Warning: failed to determine if file has been edited:", err) + cli.renderer.Warnf("Warning: failed to determine if file has been edited: %v", err) continue case editedFlag: edited = append(edited, relPath) - default: - identical = append(identical, relPath) } } - return } @@ -349,7 +365,7 @@ func isFileEdited(source, dest string) (bool, error) { if sourceInfo.Size() != destInfo.Size() { return true, nil } - // Fallback to hash comparison + hashSource, err := fileHash(source) if err != nil { return false, err @@ -373,7 +389,6 @@ func equalByteSlices(a, b []byte) bool { return true } -// Returns SHA256 hash of file at given path func fileHash(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { @@ -381,14 +396,14 @@ func fileHash(path string) ([]byte, error) { } defer f.Close() h := sha256.New() - // Use buffered copy for performance + // Use buffered copy for performance. if _, err := io.Copy(h, f); err != nil { return nil, err } return h.Sum(nil), nil } -// LoadAculConfig Loads acul_config.json once +// LoadAculConfig loads acul_config.json once. func LoadAculConfig(configPath string) (*AculConfig, error) { var configErr error aculConfigOnce.Do(func() { From 77853ac49377f400b72b8e691bcd36f46f7e254b Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 15 Sep 2025 20:03:34 +0530 Subject: [PATCH 18/58] Update go-auth0 version and docs --- docs/auth0_acul_config_get.md | 2 +- docs/auth0_acul_init.md | 2 +- go.mod | 2 +- go.sum | 4 ++-- internal/cli/acul_config.go | 2 +- internal/cli/acul_sca.go | 9 +++++---- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/auth0_acul_config_get.md b/docs/auth0_acul_config_get.md index b96d50885..b5402e170 100644 --- a/docs/auth0_acul_config_get.md +++ b/docs/auth0_acul_config_get.md @@ -16,7 +16,7 @@ auth0 acul config get [flags] ``` auth0 acul config get signup-id - auth0 acul config get login-id -f ./login.json" + auth0 acul config get login-id -f ./login-id.json ``` diff --git a/docs/auth0_acul_init.md b/docs/auth0_acul_init.md index c39283f2a..0ff5315a4 100644 --- a/docs/auth0_acul_init.md +++ b/docs/auth0_acul_init.md @@ -15,7 +15,7 @@ auth0 acul init [flags] ## Examples ``` - + acul init acul_project ``` diff --git a/go.mod b/go.mod index 0259af054..81d47a040 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/PuerkitoBio/rehttp v1.4.0 github.com/atotto/clipboard v0.1.4 - github.com/auth0/go-auth0 v1.27.1-0.20250903143702-06c2a84875fd + github.com/auth0/go-auth0 v1.27.1-0.20250908125812-5f10ae9d3e08 github.com/briandowns/spinner v1.23.2 github.com/charmbracelet/glamour v0.10.0 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e diff --git a/go.sum b/go.sum index f899e79ef..06c12ae0c 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/auth0/go-auth0 v1.27.1-0.20250903143702-06c2a84875fd h1:a2qaTQFeXQQ5Xa6+Unv+zDDXrw13HsA2KASy126J/N4= -github.com/auth0/go-auth0 v1.27.1-0.20250903143702-06c2a84875fd/go.mod h1:rLrZQWStpXQ23Uo0xRlTkXJXIR0oNVudaJWlvUnUqeI= +github.com/auth0/go-auth0 v1.27.1-0.20250908125812-5f10ae9d3e08 h1:zWfEw7nDVvFvqRTkHMyCWWkqBpBe8h7qhR2ukPShEmg= +github.com/auth0/go-auth0 v1.27.1-0.20250908125812-5f10ae9d3e08/go.mod h1:rLrZQWStpXQ23Uo0xRlTkXJXIR0oNVudaJWlvUnUqeI= github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5rljDQLKUO+cRJCnduDyn11+zGZIc9Z48= github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0/go.mod h1:6L7zgvqo0idzI7IO8de6ZC051AfXb5ipkIJ7bIA2tGA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= diff --git a/internal/cli/acul_config.go b/internal/cli/acul_config.go index bfbc77606..1509d861e 100644 --- a/internal/cli/acul_config.go +++ b/internal/cli/acul_config.go @@ -244,7 +244,7 @@ func aculConfigGetCmd(cli *cli) *cobra.Command { Short: "Get the current rendering settings for a specific screen", Long: "Get the current rendering settings for a specific screen.", Example: ` auth0 acul config get signup-id - auth0 acul config get login-id -f ./login.json"`, + auth0 acul config get login-id -f ./login-id.json`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { cli.renderer.Infof("Please select a screen ") diff --git a/internal/cli/acul_sca.go b/internal/cli/acul_sca.go index c7f3d4261..a4e082876 100644 --- a/internal/cli/acul_sca.go +++ b/internal/cli/acul_sca.go @@ -65,10 +65,11 @@ var templateFlag = Flag{ // aculInitCmd returns the cobra.Command for project initialization. func aculInitCmd(cli *cli) *cobra.Command { return &cobra.Command{ - Use: "init", - Args: cobra.MaximumNArgs(1), - Short: "Generate a new project from a template", - Long: "Generate a new project from a template.", + Use: "init", + Args: cobra.MaximumNArgs(1), + Short: "Generate a new project from a template", + Long: "Generate a new project from a template.", + Example: ` acul init acul_project`, RunE: func(cmd *cobra.Command, args []string) error { return runScaffold2(cli, cmd, args) }, From e2909db2ee6da38163921f635f4fac7e51309728 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Wed, 17 Sep 2025 14:16:40 +0530 Subject: [PATCH 19/58] refactor: rename and restructure acul scaffolding. --- .../{acul_sca.go => acul_app_scaffolding.go} | 131 +++++++++++----- internal/cli/acul_config.go | 15 +- internal/cli/acul_scaff.go | 63 +------- internal/cli/acul_screen_scaffolding.go | 143 ++++++++++-------- 4 files changed, 182 insertions(+), 170 deletions(-) rename internal/cli/{acul_sca.go => acul_app_scaffolding.go} (74%) diff --git a/internal/cli/acul_sca.go b/internal/cli/acul_app_scaffolding.go similarity index 74% rename from internal/cli/acul_sca.go rename to internal/cli/acul_app_scaffolding.go index a4e082876..a3eba2070 100644 --- a/internal/cli/acul_sca.go +++ b/internal/cli/acul_app_scaffolding.go @@ -8,7 +8,6 @@ import ( "net/http" "os" "path/filepath" - "sync" "time" "github.com/spf13/cobra" @@ -17,41 +16,88 @@ import ( "github.com/auth0/auth0-cli/internal/utils" ) -var ( - manifestLoaded Manifest // type Manifest should match your manifest schema - manifestOnce sync.Once -) +type Manifest struct { + Templates map[string]Template `json:"templates"` + Metadata Metadata `json:"metadata"` +} + +type Template struct { + Name string `json:"name"` + Description string `json:"description"` + Framework string `json:"framework"` + SDK string `json:"sdk"` + BaseFiles []string `json:"base_files"` + BaseDirectories []string `json:"base_directories"` + Screens []Screens `json:"screens"` +} + +type Screens struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Path string `json:"path"` +} -// LoadManifest Loads manifest.json once -func LoadManifest() (*Manifest, error) { +type Metadata struct { + Version string `json:"version"` + Repository string `json:"repository"` + LastUpdated string `json:"last_updated"` + Description string `json:"description"` +} + +func fetchManifest() (*Manifest, error) { + // The URL to the raw JSON file in the repository. url := "https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/monorepo-sample/manifest.json" - var manifestErr error - manifestOnce.Do(func() { - resp, err := http.Get(url) - if err != nil { - manifestErr = fmt.Errorf("cannot fetch manifest: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - manifestErr = fmt.Errorf("failed to fetch manifest: received status code %d", resp.StatusCode) - } + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("cannot fetch manifest: %w", err) + } + defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - manifestErr = fmt.Errorf("cannot read manifest body: %w", err) - } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch manifest: received status code %d", resp.StatusCode) + } - if err := json.Unmarshal(body, &manifestLoaded); err != nil { - manifestErr = fmt.Errorf("invalid manifest format: %w", err) - } - }) + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("cannot read manifest body: %w", err) + } + + var manifest Manifest + if err := json.Unmarshal(body, &manifest); err != nil { + return nil, fmt.Errorf("invalid manifest format: %w", err) + } + + return &manifest, nil +} + +// loadManifest loads manifest.json once. +func loadManifest() (*Manifest, error) { + url := "https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/monorepo-sample/manifest.json" + + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("cannot fetch manifest: %w", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch manifest: received status code %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("cannot read manifest body: %w", err) + } - if manifestErr != nil { - return nil, manifestErr + var manifest Manifest + if err := json.Unmarshal(body, &manifest); err != nil { + return nil, fmt.Errorf("invalid manifest format: %w", err) } - return &manifestLoaded, nil + return &manifest, nil } var templateFlag = Flag{ @@ -65,19 +111,20 @@ var templateFlag = Flag{ // aculInitCmd returns the cobra.Command for project initialization. func aculInitCmd(cli *cli) *cobra.Command { return &cobra.Command{ - Use: "init", - Args: cobra.MaximumNArgs(1), - Short: "Generate a new project from a template", - Long: "Generate a new project from a template.", - Example: ` acul init acul_project`, + Use: "init", + Args: cobra.MaximumNArgs(1), + Short: "Generate a new project from a template", + Long: "Generate a new project from a template.", + Example: ` auth0 acul init + auth0 acul init acul_app`, RunE: func(cmd *cobra.Command, args []string) error { - return runScaffold2(cli, cmd, args) + return runScaffold(cli, cmd, args) }, } } -func runScaffold2(cli *cli, cmd *cobra.Command, args []string) error { - manifest, err := LoadManifest() +func runScaffold(cli *cli, cmd *cobra.Command, args []string) error { + manifest, err := loadManifest() if err != nil { return err } @@ -87,7 +134,7 @@ func runScaffold2(cli *cli, cmd *cobra.Command, args []string) error { return err } - selectedScreens, err := selectScreens(manifest.Templates[chosenTemplate]) + selectedScreens, err := selectScreens(manifest.Templates[chosenTemplate].Screens) if err != nil { return err } @@ -140,9 +187,9 @@ func selectTemplate(cmd *cobra.Command, manifest *Manifest) (string, error) { return chosenTemplate, nil } -func selectScreens(template Template) ([]string, error) { +func selectScreens(screens []Screens) ([]string, error) { var screenOptions []string - for _, s := range template.Screens { + for _, s := range screens { screenOptions = append(screenOptions, s.ID) } var selectedScreens []string @@ -230,7 +277,7 @@ func copyProjectTemplateFiles(cli *cli, baseFiles []string, chosenTemplate, temp return nil } -func copyProjectScreens(cli *cli, screens []Screen, selectedScreens []string, chosenTemplate, tempUnzipDir, destDir string) error { +func copyProjectScreens(cli *cli, screens []Screens, selectedScreens []string, chosenTemplate, tempUnzipDir, destDir string) error { sourcePathPrefix := "auth0-acul-samples-monorepo-sample/" + chosenTemplate screenInfo := createScreenMap(screens) for _, s := range selectedScreens { @@ -364,8 +411,8 @@ func copyDir(src, dst string) error { }) } -func createScreenMap(screens []Screen) map[string]Screen { - screenMap := make(map[string]Screen) +func createScreenMap(screens []Screens) map[string]Screens { + screenMap := make(map[string]Screens) for _, screen := range screens { screenMap[screen.ID] = screen } diff --git a/internal/cli/acul_config.go b/internal/cli/acul_config.go index 1509d861e..7628339d8 100644 --- a/internal/cli/acul_config.go +++ b/internal/cli/acul_config.go @@ -197,7 +197,9 @@ func aculConfigGenerateCmd(cli *cli) *cobra.Command { Short: "Generate a stub config file for a Universal Login screen.", Long: "Generate a stub config file for a Universal Login screen and save it to a file.\n" + "If fileName is not provided, it will default to .json in the current directory.", - Example: ` auth0 acul config generate signup-id + Example: ` auth0 acul config generate + auth0 acul config generate --file settings.json + auth0 acul config generate signup-id auth0 acul config generate login-id --file login-settings.json`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { @@ -243,7 +245,9 @@ func aculConfigGetCmd(cli *cli) *cobra.Command { Args: cobra.MaximumNArgs(1), Short: "Get the current rendering settings for a specific screen", Long: "Get the current rendering settings for a specific screen.", - Example: ` auth0 acul config get signup-id + Example: ` auth0 acul config get + auth0 acul config get --file settings.json + auth0 acul config get signup-id auth0 acul config get login-id -f ./login-id.json`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { @@ -323,7 +327,9 @@ func aculConfigSetCmd(cli *cli) *cobra.Command { Args: cobra.MaximumNArgs(1), Short: "Set the rendering settings for a specific screen", Long: "Set the rendering settings for a specific screen.", - Example: ` auth0 acul config set signup-id --file settings.json + Example: ` auth0 acul config set + auth0 acul config set --file settings.json + auth0 acul config set signup-id --file settings.json auth0 acul config set login-id`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { @@ -465,7 +471,8 @@ func aculConfigListCmd(cli *cli) *cobra.Command { Aliases: []string{"ls"}, Short: "List Universal Login rendering configurations", Long: "List Universal Login rendering configurations with optional filters and pagination.", - Example: ` auth0 acul config list --prompt login-id --screen login --rendering-mode advanced --include-fields true --fields head_tags,context_configuration`, + Example: ` auth0 acul config list --prompt reset-password + auth0 acul config list --rendering-mode advanced --include-fields true --fields head_tags,context_configuration`, RunE: func(cmd *cobra.Command, args []string) error { params := []management.RequestOption{ management.Parameter("page", strconv.Itoa(page)), diff --git a/internal/cli/acul_scaff.go b/internal/cli/acul_scaff.go index f4186f73e..97108b77c 100644 --- a/internal/cli/acul_scaff.go +++ b/internal/cli/acul_scaff.go @@ -1,10 +1,7 @@ package cli import ( - "encoding/json" "fmt" - "io" - "net/http" "os" "os/exec" "path/filepath" @@ -16,62 +13,6 @@ import ( "github.com/auth0/auth0-cli/internal/utils" ) -type Manifest struct { - Templates map[string]Template `json:"templates"` - Metadata Metadata `json:"metadata"` -} - -type Template struct { - Name string `json:"name"` - Description string `json:"description"` - Framework string `json:"framework"` - SDK string `json:"sdk"` - BaseFiles []string `json:"base_files"` - BaseDirectories []string `json:"base_directories"` - Screens []Screen `json:"screens"` -} - -type Screen struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Path string `json:"path"` -} - -type Metadata struct { - Version string `json:"version"` - Repository string `json:"repository"` - LastUpdated string `json:"last_updated"` - Description string `json:"description"` -} - -func fetchManifest() (*Manifest, error) { - // The URL to the raw JSON file in the repository. - url := "https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/monorepo-sample/manifest.json" - - resp, err := http.Get(url) - if err != nil { - return nil, fmt.Errorf("cannot fetch manifest: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to fetch manifest: received status code %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("cannot read manifest body: %w", err) - } - - var manifest Manifest - if err := json.Unmarshal(body, &manifest); err != nil { - return nil, fmt.Errorf("invalid manifest format: %w", err) - } - - return &manifest, nil -} - // This logic goes inside your `RunE` function. func aculInitCmd1(_ *cli) *cobra.Command { cmd := &cobra.Command{ @@ -79,13 +20,13 @@ func aculInitCmd1(_ *cli) *cobra.Command { Args: cobra.MaximumNArgs(1), Short: "Generate a new project from a template", Long: `Generate a new project from a template.`, - RunE: runScaffold, + RunE: runScaffold1, } return cmd } -func runScaffold(cmd *cobra.Command, args []string) error { +func runScaffold1(cmd *cobra.Command, args []string) error { // Step 1: fetch manifest.json. manifest, err := fetchManifest() if err != nil { diff --git a/internal/cli/acul_screen_scaffolding.go b/internal/cli/acul_screen_scaffolding.go index ed5e8f850..b54cfa337 100644 --- a/internal/cli/acul_screen_scaffolding.go +++ b/internal/cli/acul_screen_scaffolding.go @@ -9,12 +9,10 @@ import ( "log" "os" "path/filepath" - "sync" - - "github.com/auth0/auth0-cli/internal/ansi" "github.com/spf13/cobra" + "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/prompt" ) @@ -26,17 +24,14 @@ var destDirFlag = Flag{ IsRequired: false, } -var ( - aculConfigOnce sync.Once - aculConfigLoaded AculConfig -) - func aculScreenAddCmd(cli *cli) *cobra.Command { var destDir string cmd := &cobra.Command{ Use: "add", Short: "Add screens to an existing project", - Long: "Add screens to an existing project.", + Long: "Add screens to an existing project. The project must have been initialized using `auth0 acul init`.", + Example: ` auth0 acul screen add ... --dir + auth0 acul screen add login-id login-password -d acul_app`, RunE: func(cmd *cobra.Command, args []string) error { pwd, err := os.Getwd() if err != nil { @@ -60,36 +55,33 @@ func aculScreenAddCmd(cli *cli) *cobra.Command { } func scaffoldAddScreen(cli *cli, args []string, destDir string) error { - manifest, err := LoadManifest() + manifest, err := loadManifest() if err != nil { return err } - aculConfig, err := LoadAculConfig(filepath.Join(destDir, "acul_config.json")) + aculConfig, err := loadAculConfig(cli, filepath.Join(destDir, "acul_config.json")) + if err != nil { + if os.IsNotExist(err) { + cli.renderer.Warnf("couldn't find acul_config.json in destination directory. Please ensure you're in the right directory or have initialized the project using `auth0 acul init`\n") + return nil + } + return err } - selectedScreens, err := chooseScreens(args, manifest, aculConfig.ChosenTemplate) + selectedScreens, err := selectAndFilterScreens(cli, args, manifest, aculConfig.ChosenTemplate, aculConfig.Screens) if err != nil { return err } - selectedScreens = filterScreensForOverwrite(selectedScreens, aculConfig.Screens) - if err = addScreensToProject(cli, destDir, aculConfig.ChosenTemplate, selectedScreens, manifest.Templates[aculConfig.ChosenTemplate]); err != nil { return err } - // Update acul_config.json with new screens. - aculConfig.Screens = append(aculConfig.Screens, selectedScreens...) - configBytes, err := json.MarshalIndent(aculConfig, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal updated acul_config.json: %w", err) - } - - if err := os.WriteFile(filepath.Join(destDir, "acul_config.json"), configBytes, 0644); err != nil { - return fmt.Errorf("failed to write updated acul_config.json: %w", err) + if err = updateAculConfigFile(destDir, aculConfig, selectedScreens); err != nil { + return err } cli.renderer.Infof(ansi.Bold(ansi.Green("Screens added successfully"))) @@ -97,22 +89,6 @@ func scaffoldAddScreen(cli *cli, args []string, destDir string) error { return nil } -// Filter out screens user does not want to overwrite. -func filterScreensForOverwrite(selectedScreens []string, existingScreens []string) []string { - var finalScreens []string - for _, s := range selectedScreens { - if screenExists(existingScreens, s) { - promptMsg := fmt.Sprintf("Screen '%s' already exists. Do you want to overwrite its directory? (y/N): ", s) - if !prompt.Confirm(promptMsg) { - continue - } - } - finalScreens = append(finalScreens, s) - } - return finalScreens -} - -// Helper to check if a screen exists in the slice. func screenExists(screens []string, target string) bool { for _, screen := range screens { if screen == target { @@ -122,18 +98,51 @@ func screenExists(screens []string, target string) bool { return false } -// Select screens: from args or prompt. -func chooseScreens(args []string, manifest *Manifest, chosenTemplate string) ([]string, error) { +func selectAndFilterScreens(cli *cli, args []string, manifest *Manifest, chosenTemplate string, existingScreens []string) ([]string, error) { + var supportedScreens []string + for _, s := range manifest.Templates[chosenTemplate].Screens { + supportedScreens = append(supportedScreens, s.ID) + } + + var initialSelected []string + if len(args) != 0 { - return args, nil + var invalidScreens []string + for _, s := range args { + if !screenExists(supportedScreens, s) { + invalidScreens = append(invalidScreens, s) + } else { + initialSelected = append(initialSelected, s) + } + } + + if len(invalidScreens) > 0 { + cli.renderer.Warnf("The following screens are either not valid or not yet supported: %v. See https://github.com/auth0-samples/auth0-acul-samples for available screens.", invalidScreens) + } + } else { + selectedScreens, err := selectScreens(manifest.Templates[chosenTemplate].Screens) + if err != nil { + return nil, err + } + initialSelected = selectedScreens } - selectedScreens, err := selectScreens(manifest.Templates[chosenTemplate]) - if err != nil { - return nil, err + if len(initialSelected) == 0 { + return nil, fmt.Errorf("no valid screens provided or selected. At least one valid screen is required to proceed") } - return selectedScreens, nil + var finalScreens []string + for _, s := range initialSelected { + if screenExists(existingScreens, s) { + promptMsg := fmt.Sprintf("Screen '%s' already exists. Do you want to overwrite its directory? (y/N): ", s) + if !prompt.Confirm(promptMsg) { + continue + } + } + finalScreens = append(finalScreens, s) + } + + return finalScreens, nil } func addScreensToProject(cli *cli, destDir, chosenTemplate string, selectedScreens []string, selectedTemplate Template) error { @@ -403,22 +412,30 @@ func fileHash(path string) ([]byte, error) { return h.Sum(nil), nil } -// LoadAculConfig loads acul_config.json once. -func LoadAculConfig(configPath string) (*AculConfig, error) { - var configErr error - aculConfigOnce.Do(func() { - b, err := os.ReadFile(configPath) - if err != nil { - configErr = err - return - } - err = json.Unmarshal(b, &aculConfigLoaded) - if err != nil { - configErr = err - } - }) - if configErr != nil { - return nil, configErr +// LoadAculConfig loads acul_config.json from the specified directory. +func loadAculConfig(cli *cli, configPath string) (*AculConfig, error) { + data, err := os.ReadFile(configPath) + if err != nil { + return nil, err + } + + var config AculConfig + err = json.Unmarshal(data, &config) + if err != nil { + return nil, err + } + + return &config, nil +} + +func updateAculConfigFile(destDir string, aculConfig *AculConfig, selectedScreens []string) error { + aculConfig.Screens = append(aculConfig.Screens, selectedScreens...) + configBytes, err := json.MarshalIndent(aculConfig, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal updated acul_config.json: %w", err) } - return &aculConfigLoaded, nil + if err := os.WriteFile(filepath.Join(destDir, "acul_config.json"), configBytes, 0644); err != nil { + return fmt.Errorf("failed to write updated acul_config.json: %w", err) + } + return nil } From 7a009fe8f5b3c3a749eff7e878f4a46e625ed831 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Wed, 17 Sep 2025 19:11:16 +0530 Subject: [PATCH 20/58] Update docs --- docs/auth0_acul_config_generate.md | 2 ++ docs/auth0_acul_config_get.md | 2 ++ docs/auth0_acul_config_list.md | 3 ++- docs/auth0_acul_config_set.md | 2 ++ docs/auth0_acul_init.md | 3 ++- docs/auth0_acul_screen_add.md | 5 +++-- 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/auth0_acul_config_generate.md b/docs/auth0_acul_config_generate.md index 560730ae1..4b582e9a3 100644 --- a/docs/auth0_acul_config_generate.md +++ b/docs/auth0_acul_config_generate.md @@ -16,6 +16,8 @@ auth0 acul config generate [flags] ## Examples ``` + auth0 acul config generate + auth0 acul config generate --file settings.json auth0 acul config generate signup-id auth0 acul config generate login-id --file login-settings.json ``` diff --git a/docs/auth0_acul_config_get.md b/docs/auth0_acul_config_get.md index b5402e170..2e8722048 100644 --- a/docs/auth0_acul_config_get.md +++ b/docs/auth0_acul_config_get.md @@ -15,6 +15,8 @@ auth0 acul config get [flags] ## Examples ``` + auth0 acul config get + auth0 acul config get --file settings.json auth0 acul config get signup-id auth0 acul config get login-id -f ./login-id.json ``` diff --git a/docs/auth0_acul_config_list.md b/docs/auth0_acul_config_list.md index 414fbad63..6eeab9b89 100644 --- a/docs/auth0_acul_config_list.md +++ b/docs/auth0_acul_config_list.md @@ -15,7 +15,8 @@ auth0 acul config list [flags] ## Examples ``` - auth0 acul config list --prompt login-id --screen login --rendering-mode advanced --include-fields true --fields head_tags,context_configuration + auth0 acul config list --prompt reset-password + auth0 acul config list --rendering-mode advanced --include-fields true --fields head_tags,context_configuration ``` diff --git a/docs/auth0_acul_config_set.md b/docs/auth0_acul_config_set.md index cb3ac82b6..2d7a25e8b 100644 --- a/docs/auth0_acul_config_set.md +++ b/docs/auth0_acul_config_set.md @@ -15,6 +15,8 @@ auth0 acul config set [flags] ## Examples ``` + auth0 acul config set + auth0 acul config set --file settings.json auth0 acul config set signup-id --file settings.json auth0 acul config set login-id ``` diff --git a/docs/auth0_acul_init.md b/docs/auth0_acul_init.md index 0ff5315a4..f8274f59a 100644 --- a/docs/auth0_acul_init.md +++ b/docs/auth0_acul_init.md @@ -15,7 +15,8 @@ auth0 acul init [flags] ## Examples ``` - acul init acul_project + auth0 acul init + auth0 acul init acul_app ``` diff --git a/docs/auth0_acul_screen_add.md b/docs/auth0_acul_screen_add.md index 55002845b..b08d6c709 100644 --- a/docs/auth0_acul_screen_add.md +++ b/docs/auth0_acul_screen_add.md @@ -5,7 +5,7 @@ has_toc: false --- # auth0 acul screen add -Add screens to an existing project. +Add screens to an existing project. The project must have been initialized using `auth0 acul init`. ## Usage ``` @@ -15,7 +15,8 @@ auth0 acul screen add [flags] ## Examples ``` - + auth0 acul screen add ... --dir + auth0 acul screen add login-id login-password -d acul_app ``` From 6a107268473e3f3f21610e9322392ffec34991ec Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Thu, 18 Sep 2025 09:07:44 +0530 Subject: [PATCH 21/58] Update docs --- docs/auth0_acul_init.md | 2 +- internal/cli/acul_app_scaffolding.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/auth0_acul_init.md b/docs/auth0_acul_init.md index f8274f59a..2f8975c4c 100644 --- a/docs/auth0_acul_init.md +++ b/docs/auth0_acul_init.md @@ -15,7 +15,7 @@ auth0 acul init [flags] ## Examples ``` - auth0 acul init + auth0 acul init auth0 acul init acul_app ``` diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index a3eba2070..6c1f4c42a 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -115,7 +115,7 @@ func aculInitCmd(cli *cli) *cobra.Command { Args: cobra.MaximumNArgs(1), Short: "Generate a new project from a template", Long: "Generate a new project from a template.", - Example: ` auth0 acul init + Example: ` auth0 acul init auth0 acul init acul_app`, RunE: func(cmd *cobra.Command, args []string) error { return runScaffold(cli, cmd, args) @@ -194,6 +194,11 @@ func selectScreens(screens []Screens) ([]string, error) { } var selectedScreens []string err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...) + + if len(selectedScreens) == 0 { + return nil, fmt.Errorf("at least one screen must be selected") + } + return selectedScreens, err } From 94a145e6e3e45e9e419cfbb8f76188d76290ab52 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Sun, 19 Oct 2025 11:04:12 +0530 Subject: [PATCH 22/58] Update branch --- .../{acul_sca.go => acul_app_scaffolding.go} | 86 ++++++++++++++++--- internal/cli/acul_scaff.go | 65 +------------- 2 files changed, 78 insertions(+), 73 deletions(-) rename internal/cli/{acul_sca.go => acul_app_scaffolding.go} (78%) diff --git a/internal/cli/acul_sca.go b/internal/cli/acul_app_scaffolding.go similarity index 78% rename from internal/cli/acul_sca.go rename to internal/cli/acul_app_scaffolding.go index bee1a34ae..61d85b64e 100644 --- a/internal/cli/acul_sca.go +++ b/internal/cli/acul_app_scaffolding.go @@ -16,6 +16,63 @@ import ( "github.com/auth0/auth0-cli/internal/utils" ) +type Manifest struct { + Templates map[string]Template `json:"templates"` + Metadata Metadata `json:"metadata"` +} + +type Template struct { + Name string `json:"name"` + Description string `json:"description"` + Framework string `json:"framework"` + SDK string `json:"sdk"` + BaseFiles []string `json:"base_files"` + BaseDirectories []string `json:"base_directories"` + Screens []Screens `json:"screens"` +} + +type Screens struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Path string `json:"path"` +} + +type Metadata struct { + Version string `json:"version"` + Repository string `json:"repository"` + LastUpdated string `json:"last_updated"` + Description string `json:"description"` +} + +// loadManifest loads manifest.json once. +func loadManifest() (*Manifest, error) { + url := "https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/monorepo-sample/manifest.json" + + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("cannot fetch manifest: %w", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch manifest: received status code %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("cannot read manifest body: %w", err) + } + + var manifest Manifest + if err := json.Unmarshal(body, &manifest); err != nil { + return nil, fmt.Errorf("invalid manifest format: %w", err) + } + + return &manifest, nil +} + var templateFlag = Flag{ Name: "Template", LongForm: "template", @@ -31,14 +88,16 @@ func aculInitCmd(cli *cli) *cobra.Command { Args: cobra.MaximumNArgs(1), Short: "Generate a new project from a template", Long: "Generate a new project from a template.", + Example: ` auth0 acul init + auth0 acul init acul_app`, RunE: func(cmd *cobra.Command, args []string) error { - return runScaffold2(cli, cmd, args) + return runScaffold(cli, cmd, args) }, } } -func runScaffold2(cli *cli, cmd *cobra.Command, args []string) error { - manifest, err := fetchManifest() +func runScaffold(cli *cli, cmd *cobra.Command, args []string) error { + manifest, err := loadManifest() if err != nil { return err } @@ -48,7 +107,7 @@ func runScaffold2(cli *cli, cmd *cobra.Command, args []string) error { return err } - selectedScreens, err := selectScreens(manifest.Templates[chosenTemplate]) + selectedScreens, err := selectScreens(manifest.Templates[chosenTemplate].Screens) if err != nil { return err } @@ -101,13 +160,18 @@ func selectTemplate(cmd *cobra.Command, manifest *Manifest) (string, error) { return chosenTemplate, nil } -func selectScreens(template Template) ([]string, error) { +func selectScreens(screens []Screens) ([]string, error) { var screenOptions []string - for _, s := range template.Screens { + for _, s := range screens { screenOptions = append(screenOptions, s.ID) } var selectedScreens []string err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...) + + if len(selectedScreens) == 0 { + return nil, fmt.Errorf("at least one screen must be selected") + } + return selectedScreens, err } @@ -191,7 +255,7 @@ func copyProjectTemplateFiles(cli *cli, baseFiles []string, chosenTemplate, temp return nil } -func copyProjectScreens(cli *cli, screens []Screen, selectedScreens []string, chosenTemplate, tempUnzipDir, destDir string) error { +func copyProjectScreens(cli *cli, screens []Screens, selectedScreens []string, chosenTemplate, tempUnzipDir, destDir string) error { sourcePathPrefix := "auth0-acul-samples-monorepo-sample/" + chosenTemplate screenInfo := createScreenMap(screens) for _, s := range selectedScreens { @@ -227,7 +291,7 @@ func copyProjectScreens(cli *cli, screens []Screen, selectedScreens []string, ch func writeAculConfig(destDir, chosenTemplate string, selectedScreens []string, manifestVersion string) error { config := AculConfig{ ChosenTemplate: chosenTemplate, - Screen: selectedScreens, + Screens: selectedScreens, InitTimestamp: time.Now().Format(time.RFC3339), AculManifestVersion: manifestVersion, } @@ -325,8 +389,8 @@ func copyDir(src, dst string) error { }) } -func createScreenMap(screens []Screen) map[string]Screen { - screenMap := make(map[string]Screen) +func createScreenMap(screens []Screens) map[string]Screens { + screenMap := make(map[string]Screens) for _, screen := range screens { screenMap[screen.ID] = screen } @@ -335,7 +399,7 @@ func createScreenMap(screens []Screen) map[string]Screen { type AculConfig struct { ChosenTemplate string `json:"chosen_template"` - Screen []string `json:"screens"` + Screens []string `json:"screens"` InitTimestamp string `json:"init_timestamp"` AculManifestVersion string `json:"acul_manifest_version"` } diff --git a/internal/cli/acul_scaff.go b/internal/cli/acul_scaff.go index f4186f73e..82a110aba 100644 --- a/internal/cli/acul_scaff.go +++ b/internal/cli/acul_scaff.go @@ -1,10 +1,7 @@ package cli import ( - "encoding/json" "fmt" - "io" - "net/http" "os" "os/exec" "path/filepath" @@ -16,62 +13,6 @@ import ( "github.com/auth0/auth0-cli/internal/utils" ) -type Manifest struct { - Templates map[string]Template `json:"templates"` - Metadata Metadata `json:"metadata"` -} - -type Template struct { - Name string `json:"name"` - Description string `json:"description"` - Framework string `json:"framework"` - SDK string `json:"sdk"` - BaseFiles []string `json:"base_files"` - BaseDirectories []string `json:"base_directories"` - Screens []Screen `json:"screens"` -} - -type Screen struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Path string `json:"path"` -} - -type Metadata struct { - Version string `json:"version"` - Repository string `json:"repository"` - LastUpdated string `json:"last_updated"` - Description string `json:"description"` -} - -func fetchManifest() (*Manifest, error) { - // The URL to the raw JSON file in the repository. - url := "https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/monorepo-sample/manifest.json" - - resp, err := http.Get(url) - if err != nil { - return nil, fmt.Errorf("cannot fetch manifest: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to fetch manifest: received status code %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("cannot read manifest body: %w", err) - } - - var manifest Manifest - if err := json.Unmarshal(body, &manifest); err != nil { - return nil, fmt.Errorf("invalid manifest format: %w", err) - } - - return &manifest, nil -} - // This logic goes inside your `RunE` function. func aculInitCmd1(_ *cli) *cobra.Command { cmd := &cobra.Command{ @@ -79,15 +20,15 @@ func aculInitCmd1(_ *cli) *cobra.Command { Args: cobra.MaximumNArgs(1), Short: "Generate a new project from a template", Long: `Generate a new project from a template.`, - RunE: runScaffold, + RunE: runScaffold1, } return cmd } -func runScaffold(cmd *cobra.Command, args []string) error { +func runScaffold1(cmd *cobra.Command, args []string) error { // Step 1: fetch manifest.json. - manifest, err := fetchManifest() + manifest, err := loadManifest() if err != nil { return err } From 74ba5069e39dcf83b77c033e5c4a529f9068418e Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 20 Oct 2025 15:47:34 +0530 Subject: [PATCH 23/58] enhance ACUL project scaffolding with Node checks and improved template selection --- internal/cli/acul_app_scaffolding.go | 113 ++++++++++++++++++++++++--- 1 file changed, 104 insertions(+), 9 deletions(-) diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index 61d85b64e..3f43439cb 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -7,11 +7,16 @@ import ( "log" "net/http" "os" + "os/exec" "path/filepath" + "regexp" + "strconv" + "strings" "time" "github.com/spf13/cobra" + "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/prompt" "github.com/auth0/auth0-cli/internal/utils" ) @@ -77,7 +82,7 @@ var templateFlag = Flag{ Name: "Template", LongForm: "template", ShortForm: "t", - Help: "Name of the template to use", + Help: "Template framework to use for your ACUL project.", IsRequired: false, } @@ -86,10 +91,12 @@ func aculInitCmd(cli *cli) *cobra.Command { return &cobra.Command{ Use: "init", Args: cobra.MaximumNArgs(1), - Short: "Generate a new project from a template", - Long: "Generate a new project from a template.", + Short: "Generate a new ACUL project from a template", + Long: `Generate a new Advanced Customizations for Universal Login (ACUL) project from a template. +This command creates a new project with your choice of framework and authentication screens (login, signup, mfa, etc.). +The generated project includes all necessary configuration and boilerplate code to get started with ACUL customizations.`, Example: ` auth0 acul init - auth0 acul init acul_app`, +auth0 acul init my_acul_app`, RunE: func(cmd *cobra.Command, args []string) error { return runScaffold(cli, cmd, args) }, @@ -97,6 +104,10 @@ func aculInitCmd(cli *cli) *cobra.Command { } func runScaffold(cli *cli, cmd *cobra.Command, args []string) error { + if err := checkNodeInstallation(); err != nil { + return err + } + manifest, err := loadManifest() if err != nil { return err @@ -146,18 +157,43 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string) error { fmt.Printf("Failed to write config: %v\n", err) } - fmt.Println("\nProject successfully created!\n" + - "Explore the sample app: https://github.com/auth0/acul-sample-app") + if err := runNpmGenerateScreenLoader(destDir); err != nil { + cli.renderer.Warnf( + "⚠️ Screen asset setup failed: %v\n"+ + "👉 Run manually: %s\n"+ + "📄 Required for: %s\n"+ + "💡 Tip: If it continues to fail, verify your Node setup and screen structure.", + err, + ansi.Bold(ansi.Cyan(fmt.Sprintf("cd %s && npm run generate:screenLoader", destDir))), + ansi.Faint(fmt.Sprintf("%s/src/utils/screen/screenLoader.ts", destDir)), + ) + } + + fmt.Printf("\nProject successfully created in '%s'!\n\n", destDir) + + fmt.Println("\n📖 Documentation:") + fmt.Println("Explore the sample app: https://github.com/auth0-samples/auth0-acul-samples") + + checkNodeVersion(cli) + return nil } func selectTemplate(cmd *cobra.Command, manifest *Manifest) (string, error) { - var chosenTemplate string - err := templateFlag.Select(cmd, &chosenTemplate, utils.FetchKeys(manifest.Templates), nil) + var templateNames []string + nameToKey := make(map[string]string) + + for key, template := range manifest.Templates { + templateNames = append(templateNames, template.Name) + nameToKey[template.Name] = key + } + + var chosenTemplateName string + err := templateFlag.Select(cmd, &chosenTemplateName, templateNames, nil) if err != nil { return "", handleInputError(err) } - return chosenTemplate, nil + return nameToKey[chosenTemplateName], nil } func selectScreens(screens []Screens) ([]string, error) { @@ -403,3 +439,62 @@ type AculConfig struct { InitTimestamp string `json:"init_timestamp"` AculManifestVersion string `json:"acul_manifest_version"` } + +// checkNodeInstallation ensures that Node is installed and accessible in the system PATH. +func checkNodeInstallation() error { + cmd := exec.Command("node", "--version") + if err := cmd.Run(); err != nil { + return fmt.Errorf("node is required but not found. Please install Node v22 or higher and try again") + } + return nil +} + +// checkNodeVersion checks the major version number of the installed Node. +func checkNodeVersion(cli *cli) { + cmd := exec.Command("node", "--version") + output, err := cmd.Output() + if err != nil { + cli.renderer.Warnf("Unable to detect Node version. Please ensure Node v22+ is installed.") + return + } + + version := strings.TrimSpace(string(output)) + re := regexp.MustCompile(`v?(\d+)\.`) + matches := re.FindStringSubmatch(version) + if len(matches) < 2 { + cli.renderer.Warnf("Unable to parse Node version: %s. Please ensure Node v22+ is installed.", version) + return + } + + if major, _ := strconv.Atoi(matches[1]); major < 22 { + fmt.Printf( + "⚠️ Node %s detected. This project requires Node v22 or higher.\n"+ + " Please upgrade to Node v22+ to run the sample app and build assets successfully.\n", + version, + ) + } +} + +// runNpmGenerateScreenLoader runs `npm run generate:screenLoader` in the given directory. +// It captures npm output and surfaces a clear, concise error message if generation fails. +func runNpmGenerateScreenLoader(destDir string) error { + cmd := exec.Command("npm", "run", "generate:screenLoader") + cmd.Dir = destDir + + output, err := cmd.CombinedOutput() + if err != nil { + // Capture a short preview of the npm output for better context. + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + summary := strings.Join(lines, "\n") + if len(lines) > 4 { + summary = strings.Join(lines[:4], "\n") + "\n..." + } + + return fmt.Errorf( + "screen loader generation failed in %s:\n%v\n\n%s", + destDir, err, summary, + ) + } + + return nil +} From b68c281c24fa736fa5393544719d9d4ee93b181c Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 20 Oct 2025 18:07:18 +0530 Subject: [PATCH 24/58] refactor screen loader generation to improve error handling --- internal/cli/acul_app_scaffolding.go | 50 +++++++++++++++------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index 3f43439cb..edd72e5ce 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -157,17 +157,7 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string) error { fmt.Printf("Failed to write config: %v\n", err) } - if err := runNpmGenerateScreenLoader(destDir); err != nil { - cli.renderer.Warnf( - "⚠️ Screen asset setup failed: %v\n"+ - "👉 Run manually: %s\n"+ - "📄 Required for: %s\n"+ - "💡 Tip: If it continues to fail, verify your Node setup and screen structure.", - err, - ansi.Bold(ansi.Cyan(fmt.Sprintf("cd %s && npm run generate:screenLoader", destDir))), - ansi.Faint(fmt.Sprintf("%s/src/utils/screen/screenLoader.ts", destDir)), - ) - } + runNpmGenerateScreenLoader(cli, destDir) fmt.Printf("\nProject successfully created in '%s'!\n\n", destDir) @@ -476,25 +466,37 @@ func checkNodeVersion(cli *cli) { } // runNpmGenerateScreenLoader runs `npm run generate:screenLoader` in the given directory. -// It captures npm output and surfaces a clear, concise error message if generation fails. -func runNpmGenerateScreenLoader(destDir string) error { +// Prints errors or warnings directly; silent if successful with no issues. +func runNpmGenerateScreenLoader(cli *cli, destDir string) { + fmt.Println(ansi.Blue("🔄 Generating screen loader...")) + cmd := exec.Command("npm", "run", "generate:screenLoader") cmd.Dir = destDir output, err := cmd.CombinedOutput() - if err != nil { - // Capture a short preview of the npm output for better context. - lines := strings.Split(strings.TrimSpace(string(output)), "\n") - summary := strings.Join(lines, "\n") - if len(lines) > 4 { - summary = strings.Join(lines[:4], "\n") + "\n..." - } + lines := strings.Split(strings.TrimSpace(string(output)), "\n") - return fmt.Errorf( - "screen loader generation failed in %s:\n%v\n\n%s", - destDir, err, summary, + // Truncate long output for readability + summary := strings.Join(lines, "\n") + if len(lines) > 5 { + summary = strings.Join(lines[:5], "\n") + "\n..." + } + + if err != nil { + cli.renderer.Warnf( + "⚠️ Screen loader generation failed: %v\n"+ + "👉 Run manually: %s\n"+ + "📄 Required for: %s\n"+ + "💡 Tip: If it continues to fail, verify your Node setup and screen structure.", + err, + ansi.Bold(ansi.Cyan(fmt.Sprintf("cd %s && npm run generate:screenLoader", destDir))), + ansi.Faint(fmt.Sprintf("%s/src/utils/screen/screenLoader.ts", destDir)), ) + return } - return nil + // Print npm output if there’s any (logs, warnings) + if len(summary) > 0 { + fmt.Println(summary) + } } From 7b0838bb450f13e149b6c2f0d80162ab67da953b Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 20 Oct 2025 18:07:52 +0530 Subject: [PATCH 25/58] fix lint --- internal/cli/acul_app_scaffolding.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index edd72e5ce..2c5ff4d50 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -476,7 +476,6 @@ func runNpmGenerateScreenLoader(cli *cli, destDir string) { output, err := cmd.CombinedOutput() lines := strings.Split(strings.TrimSpace(string(output)), "\n") - // Truncate long output for readability summary := strings.Join(lines, "\n") if len(lines) > 5 { summary = strings.Join(lines[:5], "\n") + "\n..." @@ -495,7 +494,6 @@ func runNpmGenerateScreenLoader(cli *cli, destDir string) { return } - // Print npm output if there’s any (logs, warnings) if len(summary) > 0 { fmt.Println(summary) } From 4bf2f31cc4f2367dc4e9608b4da4a7609a71d374 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Wed, 22 Oct 2025 11:52:23 +0530 Subject: [PATCH 26/58] refactor ACUL scaffolding app commands and docs --- docs/auth0_acul.md | 3 +- docs/auth0_acul_init.md | 10 +- docs/auth0_acul_init1.md | 40 -------- internal/cli/acul.go | 2 - internal/cli/acul_sacffolding_app.MD | 54 ----------- internal/cli/acul_scaff.go | 132 --------------------------- 6 files changed, 7 insertions(+), 234 deletions(-) delete mode 100644 docs/auth0_acul_init1.md delete mode 100644 internal/cli/acul_sacffolding_app.MD delete mode 100644 internal/cli/acul_scaff.go diff --git a/docs/auth0_acul.md b/docs/auth0_acul.md index 270c42e24..f8fc9e619 100644 --- a/docs/auth0_acul.md +++ b/docs/auth0_acul.md @@ -10,6 +10,5 @@ Customize the Universal Login experience. This requires a custom domain to be co ## Commands - [auth0 acul config](auth0_acul_config.md) - Configure Advanced Customizations for Universal Login screens. -- [auth0 acul init](auth0_acul_init.md) - Generate a new project from a template -- [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template +- [auth0 acul init](auth0_acul_init.md) - Generate a new ACUL project from a template diff --git a/docs/auth0_acul_init.md b/docs/auth0_acul_init.md index e6bc89b46..277da899b 100644 --- a/docs/auth0_acul_init.md +++ b/docs/auth0_acul_init.md @@ -5,7 +5,9 @@ has_toc: false --- # auth0 acul init -Generate a new project from a template. +Generate a new Advanced Customizations for Universal Login (ACUL) project from a template. +This command creates a new project with your choice of framework and authentication screens (login, signup, mfa, etc.). +The generated project includes all necessary configuration and boilerplate code to get started with ACUL customizations. ## Usage ``` @@ -15,7 +17,8 @@ auth0 acul init [flags] ## Examples ``` - + auth0 acul init +auth0 acul init my_acul_app ``` @@ -34,7 +37,6 @@ auth0 acul init [flags] ## Related Commands - [auth0 acul config](auth0_acul_config.md) - Configure Advanced Customizations for Universal Login screens. -- [auth0 acul init](auth0_acul_init.md) - Generate a new project from a template -- [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template +- [auth0 acul init](auth0_acul_init.md) - Generate a new ACUL project from a template diff --git a/docs/auth0_acul_init1.md b/docs/auth0_acul_init1.md deleted file mode 100644 index 3f5082a58..000000000 --- a/docs/auth0_acul_init1.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -layout: default -parent: auth0 acul -has_toc: false ---- -# auth0 acul init1 - -Generate a new project from a template. - -## Usage -``` -auth0 acul init1 [flags] -``` - -## Examples - -``` - -``` - - - - -## Inherited Flags - -``` - --debug Enable debug mode. - --no-color Disable colors. - --no-input Disable interactivity. - --tenant string Specific tenant to use. -``` - - -## Related Commands - -- [auth0 acul config](auth0_acul_config.md) - Configure Advanced Customizations for Universal Login screens. -- [auth0 acul init](auth0_acul_init.md) - Generate a new project from a template -- [auth0 acul init1](auth0_acul_init1.md) - Generate a new project from a template - - diff --git a/internal/cli/acul.go b/internal/cli/acul.go index 87186dd3f..b86ec82cc 100644 --- a/internal/cli/acul.go +++ b/internal/cli/acul.go @@ -10,8 +10,6 @@ func aculCmd(cli *cli) *cobra.Command { } cmd.AddCommand(aculConfigureCmd(cli)) - // Check out the ./acul_scaffolding_app.MD file for more information on the commands below. - cmd.AddCommand(aculInitCmd1(cli)) cmd.AddCommand(aculInitCmd(cli)) return cmd diff --git a/internal/cli/acul_sacffolding_app.MD b/internal/cli/acul_sacffolding_app.MD deleted file mode 100644 index ecb8e3f50..000000000 --- a/internal/cli/acul_sacffolding_app.MD +++ /dev/null @@ -1,54 +0,0 @@ -# Scaffolding Approaches: Comparison and Trade-offs - -## Method A: Git Sparse-Checkout -*File: `internal/cli/acul_scaff.go` (command `init1`)* - -**Summary:** -Initializes a git repo in the target directory, enables sparse-checkout, writes desired paths, and pulls from branch `monorepo-sample`. - -**Pros:** -- Efficient for large repos; downloads only needed paths. -- Preserves git-tracked file modes and line endings. -- Simple incremental updates (pull/merge) are possible. -- Works with private repos once user’s git is authenticated. - -**Cons:** -- Requires `git` installed and a relatively recent version for sparse-checkout. - - ---- - -## Method B: HTTP Raw + GitHub Tree API -*File: `internal/cli/acul_scaffolding.go` (command `init2`)* - -**Summary:** -Uses the GitHub Tree API to enumerate files and `raw.githubusercontent.com` to download each file individually to a target folder. - -**Pros:** -- No git dependency; pure HTTP. -- Fine-grained control over exactly which files to fetch. -- Easier sandboxing; fewer environment assumptions. - -**Cons:** -- Many HTTP requests; slower and susceptible to GitHub API rate limits. -- Loses executable bits and some metadata unless explicitly restored. -- Takes more time to download many small files [so, removed in favor of Method C]. - - ---- - -## Method C: Zip Download + Selective Copy -*File: `internal/cli/acul_sca.go` (command `init`)* - -**Summary:** -Downloads a branch zip archive once, unzips to a temp directory, then copies only base directories/files and selected screens into the target directory. - -**Pros:** -- Single network transfer; fast and API-rate-limit friendly. -- No git dependency; works in minimal environments. -- Simple to reason about and easy to clean up. -- Good for reproducible scaffolds at a specific ref (if pinned). - -**Cons:** -- Requires extra disk for the zip and the unzipped tree. -- Tightly coupled to the zip’s top-level folder name prefix. \ No newline at end of file diff --git a/internal/cli/acul_scaff.go b/internal/cli/acul_scaff.go deleted file mode 100644 index 82a110aba..000000000 --- a/internal/cli/acul_scaff.go +++ /dev/null @@ -1,132 +0,0 @@ -package cli - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "time" - - "github.com/spf13/cobra" - - "github.com/auth0/auth0-cli/internal/prompt" - "github.com/auth0/auth0-cli/internal/utils" -) - -// This logic goes inside your `RunE` function. -func aculInitCmd1(_ *cli) *cobra.Command { - cmd := &cobra.Command{ - Use: "init1", - Args: cobra.MaximumNArgs(1), - Short: "Generate a new project from a template", - Long: `Generate a new project from a template.`, - RunE: runScaffold1, - } - - return cmd -} - -func runScaffold1(cmd *cobra.Command, args []string) error { - // Step 1: fetch manifest.json. - manifest, err := loadManifest() - if err != nil { - return err - } - - // Step 2: select template. - var chosen string - promptText := prompt.SelectInput("", "Select a template", "Chosen template(Todo)", utils.FetchKeys(manifest.Templates), "react-js", true) - if err = prompt.AskOne(promptText, &chosen); err != nil { - return err - } - - // Step 3: select screens. - var screenOptions []string - template := manifest.Templates[chosen] - for _, s := range template.Screens { - screenOptions = append(screenOptions, s.ID) - } - - // Step 3: Let user select screens. - var selectedScreens []string - if err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...); err != nil { - return err - } - - // Step 3: Create project folder. - var projectDir string - if len(args) < 1 { - projectDir = "my_acul_proj1" - } else { - projectDir = args[0] - } - if err := os.MkdirAll(projectDir, 0755); err != nil { - return fmt.Errorf("failed to create project dir: %w", err) - } - - curr := time.Now() - - // Step 4: Init git repo. - repoURL := "https://github.com/auth0-samples/auth0-acul-samples.git" - if err := runGit(projectDir, "init"); err != nil { - return err - } - if err := runGit(projectDir, "remote", "add", "-f", "origin", repoURL); err != nil { - return err - } - if err := runGit(projectDir, "config", "core.sparseCheckout", "true"); err != nil { - return err - } - - // Step 5: Write sparse-checkout paths. - baseFiles := manifest.Templates[chosen].BaseFiles - baseDirectories := manifest.Templates[chosen].BaseDirectories - - var paths []string - paths = append(paths, baseFiles...) - paths = append(paths, baseDirectories...) - - for _, scr := range template.Screens { - for _, chosenScreen := range selectedScreens { - if scr.ID == chosenScreen { - paths = append(paths, scr.Path) - } - } - } - - sparseFile := filepath.Join(projectDir, ".git", "info", "sparse-checkout") - - f, err := os.Create(sparseFile) - if err != nil { - return fmt.Errorf("failed to write sparse-checkout file: %w", err) - } - - for _, p := range paths { - _, _ = f.WriteString(p + "\n") - } - - f.Close() - - // Step 6: Pull only sparse files. - if err := runGit(projectDir, "pull", "origin", "monorepo-sample"); err != nil { - return err - } - - // Step 7: Clean up .git. - if err := os.RemoveAll(filepath.Join(projectDir, ".git")); err != nil { - return fmt.Errorf("failed to clean up git metadata: %w", err) - } - - fmt.Println(time.Since(curr)) - - fmt.Printf("✅ Project scaffolded successfully in %s\n", projectDir) - return nil -} - -func runGit(dir string, args ...string) error { - cmd := exec.Command("git", args...) - cmd.Dir = dir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} From f0c01f515c17494f11f4224afaba0c59fb739693 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Wed, 22 Oct 2025 12:33:16 +0530 Subject: [PATCH 27/58] enhance ACUL scaffolding with user guidance and next steps --- docs/auth0_acul_init.md | 2 +- internal/cli/acul_app_scaffolding.go | 33 +++++++++++++++++++++---- internal/cli/acul_screen_scaffolding.go | 18 +++++++++++++- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/docs/auth0_acul_init.md b/docs/auth0_acul_init.md index 6db142387..74d7383b5 100644 --- a/docs/auth0_acul_init.md +++ b/docs/auth0_acul_init.md @@ -18,7 +18,7 @@ auth0 acul init [flags] ``` auth0 acul init - auth0 acul init acul_app +auth0 acul init my_acul_app ``` diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index 2c5ff4d50..d3c39f9a0 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -161,11 +161,22 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string) error { fmt.Printf("\nProject successfully created in '%s'!\n\n", destDir) - fmt.Println("\n📖 Documentation:") + fmt.Println("📖 Documentation:") fmt.Println("Explore the sample app: https://github.com/auth0-samples/auth0-acul-samples") checkNodeVersion(cli) + // Show next steps and related commands + fmt.Println("\n" + ansi.Bold("🚀 Next Steps:")) + fmt.Printf(" 1. %s\n", ansi.Cyan(fmt.Sprintf("cd %s", destDir))) + fmt.Printf(" 2. %s\n", ansi.Cyan("npm install")) + fmt.Printf(" 3. %s\n", ansi.Cyan("npm run dev")) + fmt.Println() + + showAculCommands() + + fmt.Printf("💡 %s: %s\n", ansi.Bold("Tip"), "Use 'auth0 acul --help' to see all available commands") + return nil } @@ -423,6 +434,17 @@ func createScreenMap(screens []Screens) map[string]Screens { return screenMap } +// showAculCommands displays available ACUL commands for user guidance +func showAculCommands() { + fmt.Println(ansi.Bold("📋 Available Commands:")) + fmt.Printf(" • %s - Add more screens to your project\n", ansi.Green("auth0 acul screen add ")) + fmt.Printf(" • %s - Generate configuration files\n", ansi.Green("auth0 acul config generate ")) + fmt.Printf(" • %s - Download current settings\n", ansi.Green("auth0 acul config get ")) + fmt.Printf(" • %s - Upload customizations\n", ansi.Green("auth0 acul config set ")) + fmt.Printf(" • %s - View available screens\n", ansi.Green("auth0 acul config list")) + fmt.Println() +} + type AculConfig struct { ChosenTemplate string `json:"chosen_template"` Screens []string `json:"screens"` @@ -468,7 +490,6 @@ func checkNodeVersion(cli *cli) { // runNpmGenerateScreenLoader runs `npm run generate:screenLoader` in the given directory. // Prints errors or warnings directly; silent if successful with no issues. func runNpmGenerateScreenLoader(cli *cli, destDir string) { - fmt.Println(ansi.Blue("🔄 Generating screen loader...")) cmd := exec.Command("npm", "run", "generate:screenLoader") cmd.Dir = destDir @@ -491,10 +512,12 @@ func runNpmGenerateScreenLoader(cli *cli, destDir string) { ansi.Bold(ansi.Cyan(fmt.Sprintf("cd %s && npm run generate:screenLoader", destDir))), ansi.Faint(fmt.Sprintf("%s/src/utils/screen/screenLoader.ts", destDir)), ) + + if len(summary) > 0 { + fmt.Println(summary) + } + return } - if len(summary) > 0 { - fmt.Println(summary) - } } diff --git a/internal/cli/acul_screen_scaffolding.go b/internal/cli/acul_screen_scaffolding.go index b54cfa337..89cdd497c 100644 --- a/internal/cli/acul_screen_scaffolding.go +++ b/internal/cli/acul_screen_scaffolding.go @@ -80,11 +80,16 @@ func scaffoldAddScreen(cli *cli, args []string, destDir string) error { return err } + runNpmGenerateScreenLoader(cli, destDir) + if err = updateAculConfigFile(destDir, aculConfig, selectedScreens); err != nil { return err } - cli.renderer.Infof(ansi.Bold(ansi.Green("Screens added successfully"))) + cli.renderer.Infof(ansi.Bold(ansi.Green("✅ Screens added successfully!"))) + + // Show related commands and next steps + showAculScreenCommands() return nil } @@ -439,3 +444,14 @@ func updateAculConfigFile(destDir string, aculConfig *AculConfig, selectedScreen } return nil } + +// showAculScreenCommands displays available ACUL commands for user guidance +func showAculScreenCommands() { + fmt.Println(ansi.Bold("📋 Available Commands:")) + fmt.Printf(" • %s - Add more screens\n", ansi.Green("auth0 acul screen add ")) + fmt.Printf(" • %s - Generate configuration files\n", ansi.Green("auth0 acul config generate ")) + fmt.Printf(" • %s - Download current settings\n", ansi.Green("auth0 acul config get ")) + fmt.Printf(" • %s - Upload customizations\n", ansi.Green("auth0 acul config set ")) + fmt.Printf(" • %s - View available screens\n", ansi.Green("auth0 acul config list")) + fmt.Println() +} From d499a95df3483a8f288a716e2c0b014f668bd38b Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Thu, 23 Oct 2025 12:12:52 +0530 Subject: [PATCH 28/58] enhance ACUL scaffolding with improved user feedback --- internal/cli/acul_app_scaffolding.go | 150 ++++++++++++++++-------- internal/cli/acul_screen_scaffolding.go | 4 +- 2 files changed, 103 insertions(+), 51 deletions(-) diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index d3c39f9a0..8b9c344e2 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -159,23 +159,29 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string) error { runNpmGenerateScreenLoader(cli, destDir) - fmt.Printf("\nProject successfully created in '%s'!\n\n", destDir) + cli.renderer.Output("") + cli.renderer.Infof("%s Project successfully created in %s!", + ansi.Bold(ansi.Green("🎉")), ansi.Bold(ansi.Cyan(fmt.Sprintf("'%s'", destDir)))) + cli.renderer.Output("") - fmt.Println("📖 Documentation:") - fmt.Println("Explore the sample app: https://github.com/auth0-samples/auth0-acul-samples") + cli.renderer.Infof("%s Documentation:", ansi.Bold("📖")) + cli.renderer.Infof(" Explore the sample app: %s", + ansi.Blue("https://github.com/auth0-samples/auth0-acul-samples")) + cli.renderer.Output("") checkNodeVersion(cli) - // Show next steps and related commands - fmt.Println("\n" + ansi.Bold("🚀 Next Steps:")) - fmt.Printf(" 1. %s\n", ansi.Cyan(fmt.Sprintf("cd %s", destDir))) - fmt.Printf(" 2. %s\n", ansi.Cyan("npm install")) - fmt.Printf(" 3. %s\n", ansi.Cyan("npm run dev")) - fmt.Println() + // Show next steps and related commands. + cli.renderer.Infof("%s Next Steps:", ansi.Bold("🚀")) + cli.renderer.Infof(" 1. %s", ansi.Bold(ansi.Cyan(fmt.Sprintf("cd %s", destDir)))) + cli.renderer.Infof(" 2. %s", ansi.Bold(ansi.Cyan("npm install"))) + cli.renderer.Infof(" 3. %s", ansi.Bold(ansi.Cyan("npm run dev"))) + cli.renderer.Output("") showAculCommands() - fmt.Printf("💡 %s: %s\n", ansi.Bold("Tip"), "Use 'auth0 acul --help' to see all available commands") + cli.renderer.Infof("%s %s: Use %s to see all available commands", + ansi.Bold("💡"), ansi.Bold("Tip"), ansi.Bold(ansi.Cyan("'auth0 acul --help'"))) return nil } @@ -249,7 +255,8 @@ func copyTemplateBaseDirs(cli *cli, baseDirs []string, chosenTemplate, tempUnzip destPath := filepath.Join(destDir, relPath) if _, err = os.Stat(srcPath); os.IsNotExist(err) { - cli.renderer.Warnf("Warning: Source directory does not exist: %s", srcPath) + cli.renderer.Warnf("%s Source directory does not exist: %s", + ansi.Bold(ansi.Yellow("⚠️")), ansi.Faint(srcPath)) continue } @@ -274,13 +281,15 @@ func copyProjectTemplateFiles(cli *cli, baseFiles []string, chosenTemplate, temp destPath := filepath.Join(destDir, relPath) if _, err = os.Stat(srcPath); os.IsNotExist(err) { - cli.renderer.Warnf("Warning: Source file does not exist: %s", srcPath) + cli.renderer.Warnf("%s Source file does not exist: %s", + ansi.Bold(ansi.Yellow("⚠️")), ansi.Faint(srcPath)) continue } parentDir := filepath.Dir(destPath) if err := os.MkdirAll(parentDir, 0755); err != nil { - cli.renderer.Warnf("Error creating parent directory for %s: %v", baseFile, err) + cli.renderer.Warnf("%s Error creating parent directory for %s: %v", + ansi.Bold(ansi.Red("❌")), ansi.Bold(baseFile), err) continue } @@ -307,13 +316,15 @@ func copyProjectScreens(cli *cli, screens []Screens, selectedScreens []string, c destPath := filepath.Join(destDir, relPath) if _, err = os.Stat(srcPath); os.IsNotExist(err) { - cli.renderer.Warnf("Warning: Source directory does not exist: %s", srcPath) + cli.renderer.Warnf("%s Source directory does not exist: %s", + ansi.Bold(ansi.Yellow("⚠️")), ansi.Faint(srcPath)) continue } parentDir := filepath.Dir(destPath) if err := os.MkdirAll(parentDir, 0755); err != nil { - cli.renderer.Warnf("Error creating parent directory for %s: %v", screen.Path, err) + cli.renderer.Warnf("%s Error creating parent directory for %s: %v", + ansi.Bold(ansi.Red("❌")), ansi.Bold(screen.Path), err) continue } @@ -434,14 +445,19 @@ func createScreenMap(screens []Screens) map[string]Screens { return screenMap } -// showAculCommands displays available ACUL commands for user guidance +// showAculCommands displays available ACUL commands for user guidance. func showAculCommands() { - fmt.Println(ansi.Bold("📋 Available Commands:")) - fmt.Printf(" • %s - Add more screens to your project\n", ansi.Green("auth0 acul screen add ")) - fmt.Printf(" • %s - Generate configuration files\n", ansi.Green("auth0 acul config generate ")) - fmt.Printf(" • %s - Download current settings\n", ansi.Green("auth0 acul config get ")) - fmt.Printf(" • %s - Upload customizations\n", ansi.Green("auth0 acul config set ")) - fmt.Printf(" • %s - View available screens\n", ansi.Green("auth0 acul config list")) + fmt.Printf("%s Available Commands:\n", ansi.Bold("📋")) + fmt.Printf(" %s - Add more screens to your project\n", + ansi.Bold(ansi.Green("auth0 acul screen add "))) + fmt.Printf(" %s - Generate configuration files\n", + ansi.Bold(ansi.Green("auth0 acul config generate "))) + fmt.Printf(" %s - Download current settings\n", + ansi.Bold(ansi.Green("auth0 acul config get "))) + fmt.Printf(" %s - Upload customizations\n", + ansi.Bold(ansi.Green("auth0 acul config set "))) + fmt.Printf(" %s - View available screens\n", + ansi.Bold(ansi.Green("auth0 acul config list"))) fmt.Println() } @@ -456,7 +472,13 @@ type AculConfig struct { func checkNodeInstallation() error { cmd := exec.Command("node", "--version") if err := cmd.Run(); err != nil { - return fmt.Errorf("node is required but not found. Please install Node v22 or higher and try again") + return fmt.Errorf("%s Node.js is required but not found.\n"+ + " %s Please install Node.js v22 or higher from: %s\n"+ + " %s Then try running this command again", + ansi.Bold(ansi.Red("❌")), + ansi.Yellow("→"), + ansi.Blue("https://nodejs.org/"), + ansi.Yellow("→")) } return nil } @@ -466,7 +488,8 @@ func checkNodeVersion(cli *cli) { cmd := exec.Command("node", "--version") output, err := cmd.Output() if err != nil { - cli.renderer.Warnf("Unable to detect Node version. Please ensure Node v22+ is installed.") + cli.renderer.Warnf("%s Unable to detect Node version. Please ensure Node v22+ is installed.", + ansi.Bold(ansi.Yellow("⚠️"))) return } @@ -474,50 +497,79 @@ func checkNodeVersion(cli *cli) { re := regexp.MustCompile(`v?(\d+)\.`) matches := re.FindStringSubmatch(version) if len(matches) < 2 { - cli.renderer.Warnf("Unable to parse Node version: %s. Please ensure Node v22+ is installed.", version) + cli.renderer.Warnf("%s Unable to parse Node version: %s. Please ensure Node v22+ is installed.", + ansi.Bold(ansi.Yellow("⚠️")), ansi.Bold(version)) return } if major, _ := strconv.Atoi(matches[1]); major < 22 { - fmt.Printf( - "⚠️ Node %s detected. This project requires Node v22 or higher.\n"+ - " Please upgrade to Node v22+ to run the sample app and build assets successfully.\n", - version, - ) + cli.renderer.Output("") + cli.renderer.Warnf("⚠️ Node %s detected. This project requires Node v22 or higher.", + ansi.Bold(version)) + cli.renderer.Warnf(" Please upgrade to Node v22+ to run the sample app and build assets successfully.") + cli.renderer.Output("") } } // runNpmGenerateScreenLoader runs `npm run generate:screenLoader` in the given directory. -// Prints errors or warnings directly; silent if successful with no issues. func runNpmGenerateScreenLoader(cli *cli, destDir string) { + cli.renderer.Infof("%s Generating screen loader...", ansi.Blue("🔄")) cmd := exec.Command("npm", "run", "generate:screenLoader") cmd.Dir = destDir output, err := cmd.CombinedOutput() - lines := strings.Split(strings.TrimSpace(string(output)), "\n") - - summary := strings.Join(lines, "\n") - if len(lines) > 5 { - summary = strings.Join(lines[:5], "\n") + "\n..." - } + outputStr := strings.TrimSpace(string(output)) if err != nil { - cli.renderer.Warnf( - "⚠️ Screen loader generation failed: %v\n"+ - "👉 Run manually: %s\n"+ - "📄 Required for: %s\n"+ - "💡 Tip: If it continues to fail, verify your Node setup and screen structure.", - err, - ansi.Bold(ansi.Cyan(fmt.Sprintf("cd %s && npm run generate:screenLoader", destDir))), - ansi.Faint(fmt.Sprintf("%s/src/utils/screen/screenLoader.ts", destDir)), - ) - - if len(summary) > 0 { - fmt.Println(summary) + cli.renderer.Output("") + cli.renderer.Errorf("%s Screen loader generation failed", ansi.Bold(ansi.Red("❌"))) + cli.renderer.Output("") + + cli.renderer.Warnf("%s Run manually:", ansi.Bold(ansi.Yellow("💡"))) + cli.renderer.Infof(" %s", ansi.Bold(ansi.Cyan(fmt.Sprintf("cd %s && npm run generate:screenLoader", destDir)))) + cli.renderer.Output("") + + cli.renderer.Warnf("%s Required for:", ansi.Bold(ansi.Yellow("📄"))) + cli.renderer.Infof(" %s", ansi.Faint(fmt.Sprintf("%s/src/utils/screen/screenLoader.ts", destDir))) + cli.renderer.Output("") + + if outputStr != "" { + lines := strings.Split(outputStr, "\n") + cli.renderer.Warnf("%s Error details:", ansi.Bold(ansi.Yellow("⚠️"))) + if len(lines) > 3 { + for i, line := range lines[:3] { + cli.renderer.Infof(" %d. %s", i+1, ansi.Faint(line)) + } + cli.renderer.Infof(" %s", ansi.Faint("... (truncated)")) + } else { + for i, line := range lines { + cli.renderer.Infof(" %d. %s", i+1, ansi.Faint(line)) + } + } + cli.renderer.Output("") } + cli.renderer.Warnf("%s If it continues to fail, verify your Node setup and screen structure.", + ansi.Bold(ansi.Yellow("💡"))) + cli.renderer.Output("") return } + // Success case. + cli.renderer.Infof("%s Screen loader generated successfully!", ansi.Bold(ansi.Green("✅"))) + + // Show any npm output as info. + if outputStr != "" && len(strings.TrimSpace(outputStr)) > 0 { + lines := strings.Split(outputStr, "\n") + for _, line := range lines { + if strings.TrimSpace(line) != "" { + if strings.Contains(line, "warn") || strings.Contains(line, "warning") { + cli.renderer.Warnf(" %s", ansi.Faint(line)) + } else { + cli.renderer.Infof(" %s", ansi.Faint(line)) + } + } + } + } } diff --git a/internal/cli/acul_screen_scaffolding.go b/internal/cli/acul_screen_scaffolding.go index 89cdd497c..22b3ad427 100644 --- a/internal/cli/acul_screen_scaffolding.go +++ b/internal/cli/acul_screen_scaffolding.go @@ -88,7 +88,7 @@ func scaffoldAddScreen(cli *cli, args []string, destDir string) error { cli.renderer.Infof(ansi.Bold(ansi.Green("✅ Screens added successfully!"))) - // Show related commands and next steps + // Show related commands and next steps. showAculScreenCommands() return nil @@ -445,7 +445,7 @@ func updateAculConfigFile(destDir string, aculConfig *AculConfig, selectedScreen return nil } -// showAculScreenCommands displays available ACUL commands for user guidance +// showAculScreenCommands displays available ACUL commands for user guidance. func showAculScreenCommands() { fmt.Println(ansi.Bold("📋 Available Commands:")) fmt.Printf(" • %s - Add more screens\n", ansi.Green("auth0 acul screen add ")) From d406430258d6e07d40fb999d0c7fcea09c8ffc2a Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Thu, 23 Oct 2025 14:28:55 +0530 Subject: [PATCH 29/58] refactor ACUL scaffolding --- internal/cli/acul_app_scaffolding.go | 90 ++++++++----------------- internal/cli/acul_screen_scaffolding.go | 16 +---- 2 files changed, 30 insertions(+), 76 deletions(-) diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index 8b9c344e2..5daa42de1 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -172,10 +172,10 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string) error { checkNodeVersion(cli) // Show next steps and related commands. - cli.renderer.Infof("%s Next Steps:", ansi.Bold("🚀")) - cli.renderer.Infof(" 1. %s", ansi.Bold(ansi.Cyan(fmt.Sprintf("cd %s", destDir)))) - cli.renderer.Infof(" 2. %s", ansi.Bold(ansi.Cyan("npm install"))) - cli.renderer.Infof(" 3. %s", ansi.Bold(ansi.Cyan("npm run dev"))) + cli.renderer.Infof("%s Next Steps: Navigate to %s and run: 🚀", ansi.Bold(ansi.Cyan(destDir))) + cli.renderer.Infof(" 1. %s", ansi.Bold(ansi.Cyan("npm install"))) + cli.renderer.Infof(" 2. %s", ansi.Bold(ansi.Cyan("npm run build"))) + cli.renderer.Infof(" 3. %s", ansi.Bold(ansi.Cyan("npm run screen dev"))) cli.renderer.Output("") showAculCommands() @@ -473,12 +473,11 @@ func checkNodeInstallation() error { cmd := exec.Command("node", "--version") if err := cmd.Run(); err != nil { return fmt.Errorf("%s Node.js is required but not found.\n"+ - " %s Please install Node.js v22 or higher from: %s\n"+ + " %s Please install Node.js v22 or higher \n"+ " %s Then try running this command again", ansi.Bold(ansi.Red("❌")), ansi.Yellow("→"), - ansi.Blue("https://nodejs.org/"), - ansi.Yellow("→")) + ansi.Blue("→")) } return nil } @@ -488,8 +487,7 @@ func checkNodeVersion(cli *cli) { cmd := exec.Command("node", "--version") output, err := cmd.Output() if err != nil { - cli.renderer.Warnf("%s Unable to detect Node version. Please ensure Node v22+ is installed.", - ansi.Bold(ansi.Yellow("⚠️"))) + cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf("Unable to detect Node version. Please ensure Node v22+ is installed."))) return } @@ -497,79 +495,47 @@ func checkNodeVersion(cli *cli) { re := regexp.MustCompile(`v?(\d+)\.`) matches := re.FindStringSubmatch(version) if len(matches) < 2 { - cli.renderer.Warnf("%s Unable to parse Node version: %s. Please ensure Node v22+ is installed.", - ansi.Bold(ansi.Yellow("⚠️")), ansi.Bold(version)) + cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf("Unable to parse Node version: %s. Please ensure Node v22+ is installed.", version))) return } if major, _ := strconv.Atoi(matches[1]); major < 22 { cli.renderer.Output("") - cli.renderer.Warnf("⚠️ Node %s detected. This project requires Node v22 or higher.", - ansi.Bold(version)) - cli.renderer.Warnf(" Please upgrade to Node v22+ to run the sample app and build assets successfully.") + cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf(" Node %s detected. This project requires Node %s or higher.", + version, "v22"))) cli.renderer.Output("") } } // runNpmGenerateScreenLoader runs `npm run generate:screenLoader` in the given directory. func runNpmGenerateScreenLoader(cli *cli, destDir string) { - cli.renderer.Infof("%s Generating screen loader...", ansi.Blue("🔄")) - cmd := exec.Command("npm", "run", "generate:screenLoader") cmd.Dir = destDir output, err := cmd.CombinedOutput() - outputStr := strings.TrimSpace(string(output)) - - if err != nil { - cli.renderer.Output("") - cli.renderer.Errorf("%s Screen loader generation failed", ansi.Bold(ansi.Red("❌"))) - cli.renderer.Output("") - - cli.renderer.Warnf("%s Run manually:", ansi.Bold(ansi.Yellow("💡"))) - cli.renderer.Infof(" %s", ansi.Bold(ansi.Cyan(fmt.Sprintf("cd %s && npm run generate:screenLoader", destDir)))) - cli.renderer.Output("") + lines := strings.Split(strings.TrimSpace(string(output)), "\n") - cli.renderer.Warnf("%s Required for:", ansi.Bold(ansi.Yellow("📄"))) - cli.renderer.Infof(" %s", ansi.Faint(fmt.Sprintf("%s/src/utils/screen/screenLoader.ts", destDir))) - cli.renderer.Output("") + summary := strings.Join(lines, "\n") + if len(lines) > 5 { + summary = strings.Join(lines[:5], "\n") + "\n..." + } - if outputStr != "" { - lines := strings.Split(outputStr, "\n") - cli.renderer.Warnf("%s Error details:", ansi.Bold(ansi.Yellow("⚠️"))) - if len(lines) > 3 { - for i, line := range lines[:3] { - cli.renderer.Infof(" %d. %s", i+1, ansi.Faint(line)) - } - cli.renderer.Infof(" %s", ansi.Faint("... (truncated)")) - } else { - for i, line := range lines { - cli.renderer.Infof(" %d. %s", i+1, ansi.Faint(line)) - } - } - cli.renderer.Output("") + if err != nil { + cli.renderer.Warnf( + "⚠️ Screen loader generation failed: %v\n"+ + "👉 Run manually: %s\n"+ + "📄 Required for: %s\n"+ + "💡 Tip: If it continues to fail, verify your Node setup and screen structure.", + err, + ansi.Bold(ansi.Cyan(fmt.Sprintf("cd %s && npm run generate:screenLoader", destDir))), + ansi.Faint(fmt.Sprintf("%s/src/utils/screen/screenLoader.ts", destDir)), + ) + + if len(summary) > 0 { + fmt.Println(summary) } - cli.renderer.Warnf("%s If it continues to fail, verify your Node setup and screen structure.", - ansi.Bold(ansi.Yellow("💡"))) - cli.renderer.Output("") return } - // Success case. - cli.renderer.Infof("%s Screen loader generated successfully!", ansi.Bold(ansi.Green("✅"))) - - // Show any npm output as info. - if outputStr != "" && len(strings.TrimSpace(outputStr)) > 0 { - lines := strings.Split(outputStr, "\n") - for _, line := range lines { - if strings.TrimSpace(line) != "" { - if strings.Contains(line, "warn") || strings.Contains(line, "warning") { - cli.renderer.Warnf(" %s", ansi.Faint(line)) - } else { - cli.renderer.Infof(" %s", ansi.Faint(line)) - } - } - } - } } diff --git a/internal/cli/acul_screen_scaffolding.go b/internal/cli/acul_screen_scaffolding.go index 22b3ad427..1e3330ff7 100644 --- a/internal/cli/acul_screen_scaffolding.go +++ b/internal/cli/acul_screen_scaffolding.go @@ -86,10 +86,9 @@ func scaffoldAddScreen(cli *cli, args []string, destDir string) error { return err } - cli.renderer.Infof(ansi.Bold(ansi.Green("✅ Screens added successfully!"))) + cli.renderer.Infof(ansi.Bold(ansi.Green("Screens added successfully"))) - // Show related commands and next steps. - showAculScreenCommands() + showAculCommands() return nil } @@ -444,14 +443,3 @@ func updateAculConfigFile(destDir string, aculConfig *AculConfig, selectedScreen } return nil } - -// showAculScreenCommands displays available ACUL commands for user guidance. -func showAculScreenCommands() { - fmt.Println(ansi.Bold("📋 Available Commands:")) - fmt.Printf(" • %s - Add more screens\n", ansi.Green("auth0 acul screen add ")) - fmt.Printf(" • %s - Generate configuration files\n", ansi.Green("auth0 acul config generate ")) - fmt.Printf(" • %s - Download current settings\n", ansi.Green("auth0 acul config get ")) - fmt.Printf(" • %s - Upload customizations\n", ansi.Green("auth0 acul config set ")) - fmt.Printf(" • %s - View available screens\n", ansi.Green("auth0 acul config list")) - fmt.Println() -} From 87086428a85e4d5a54920297974424bdc41254a9 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Thu, 23 Oct 2025 18:20:48 +0530 Subject: [PATCH 30/58] refactor ACUL scaffolding --- docs/auth0_acul_config_get.md | 2 +- internal/cli/acul_app_scaffolding.go | 5 ++--- internal/cli/acul_config.go | 19 ++++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/auth0_acul_config_get.md b/docs/auth0_acul_config_get.md index 2e8722048..8ee5c30af 100644 --- a/docs/auth0_acul_config_get.md +++ b/docs/auth0_acul_config_get.md @@ -18,7 +18,7 @@ auth0 acul config get [flags] auth0 acul config get auth0 acul config get --file settings.json auth0 acul config get signup-id - auth0 acul config get login-id -f ./login-id.json + auth0 acul config get login-id -f ./acul_config/login-id.json ``` diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index 5daa42de1..15bca3d47 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -450,7 +450,7 @@ func showAculCommands() { fmt.Printf("%s Available Commands:\n", ansi.Bold("📋")) fmt.Printf(" %s - Add more screens to your project\n", ansi.Bold(ansi.Green("auth0 acul screen add "))) - fmt.Printf(" %s - Generate configuration files\n", + fmt.Printf(" %s - Generate a stub config file\n", ansi.Bold(ansi.Green("auth0 acul config generate "))) fmt.Printf(" %s - Download current settings\n", ansi.Bold(ansi.Green("auth0 acul config get "))) @@ -487,7 +487,7 @@ func checkNodeVersion(cli *cli) { cmd := exec.Command("node", "--version") output, err := cmd.Output() if err != nil { - cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf("Unable to detect Node version. Please ensure Node v22+ is installed."))) + cli.renderer.Warnf(ansi.Yellow("Unable to detect Node version. Please ensure Node v22+ is installed.")) return } @@ -537,5 +537,4 @@ func runNpmGenerateScreenLoader(cli *cli, destDir string) { return } - } diff --git a/internal/cli/acul_config.go b/internal/cli/acul_config.go index 3d35d78c3..9de372a7b 100644 --- a/internal/cli/acul_config.go +++ b/internal/cli/acul_config.go @@ -179,10 +179,10 @@ type aculConfigInput struct { // ensureConfigFilePath sets a default config file path if none is provided and creates the config directory. func ensureConfigFilePath(input *aculConfigInput, cli *cli) error { if input.filePath == "" { - input.filePath = fmt.Sprintf("config/%s.json", input.screenName) + input.filePath = fmt.Sprintf("acul_config/%s.json", input.screenName) cli.renderer.Warnf("No configuration file path specified. Defaulting to '%s'.", ansi.Green(input.filePath)) } - if err := os.MkdirAll("config", 0755); err != nil { + if err := os.MkdirAll("acul_config", 0755); err != nil { return fmt.Errorf("could not create config directory: %w", err) } return nil @@ -241,12 +241,11 @@ func aculConfigGenerateCmd(cli *cli) *cobra.Command { return fmt.Errorf("could not write config: %w", err) } - cli.renderer.Infof("Configuration generated at '%s'.\n"+ - " Review the documentation for configuring screens to use ACUL\n"+ - " https://auth0.com/docs/customize/login-pages/advanced-customizations/getting-started/configure-acul-screens\n", - ansi.Green(input.filePath)) + cli.renderer.Infof("Configuration generated at '%s'", ansi.Green(input.filePath)) + + cli.renderer.Output("Learn more about configuring ACUL screens https://auth0.com/docs/customize/login-pages/advanced-customizations/getting-started/configure-acul-screens") + cli.renderer.Output(ansi.Yellow("💡 Tip: Use `auth0 acul config get` to fetch remote rendering settings or `auth0 acul config set` to sync local configs.")) - cli.renderer.Output(ansi.Cyan("📖 Customization Guide: https://github.com/auth0/auth0-cli/blob/main/CUSTOMIZATION_GUIDE.md")) return nil }, } @@ -266,7 +265,7 @@ func aculConfigGetCmd(cli *cli) *cobra.Command { Example: ` auth0 acul config get auth0 acul config get --file settings.json auth0 acul config get signup-id - auth0 acul config get login-id -f ./login-id.json`, + auth0 acul config get login-id -f ./acul_config/login-id.json`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { cli.renderer.Output(ansi.Yellow("🔍 Type any part of the screen name (e.g., 'login', 'mfa') to filter options.")) @@ -284,6 +283,8 @@ func aculConfigGetCmd(cli *cli) *cobra.Command { if existingRenderSettings == nil { cli.renderer.Warnf("No rendering settings found for screen '%s' in tenant '%s'.", ansi.Green(input.screenName), ansi.Blue(cli.tenant)) + cli.renderer.Output(ansi.Yellow("💡 Tip: Use `auth0 acul config generate` to generate a stub config file or `auth0 acul config set` to sync local configs.")) + cli.renderer.Output(ansi.Cyan("📖 Customization Guide: https://github.com/auth0/auth0-cli/blob/main/CUSTOMIZATION_GUIDE.md")) return nil } @@ -409,7 +410,7 @@ func fetchRenderSettings(cmd *cobra.Command, cli *cli, input aculConfigInput) (* } // Case 2: No file path provided, default to config/.json. - defaultFilePath := fmt.Sprintf("config/%s.json", input.screenName) + defaultFilePath := fmt.Sprintf("acul_config/%s.json", input.screenName) data, err := os.ReadFile(defaultFilePath) if err == nil { cli.renderer.Warnf("No file path specified. Defaulting to '%s'.", ansi.Green(defaultFilePath)) From 867c98110c1ba3e19e293830400b7eb38c8d3743 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Fri, 24 Oct 2025 09:47:26 +0530 Subject: [PATCH 31/58] refactor ACUL scaffolding to filter out regenerated screenLoader.ts file --- internal/cli/acul_screen_scaffolding.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/cli/acul_screen_scaffolding.go b/internal/cli/acul_screen_scaffolding.go index 1e3330ff7..73a0e7c0e 100644 --- a/internal/cli/acul_screen_scaffolding.go +++ b/internal/cli/acul_screen_scaffolding.go @@ -174,7 +174,10 @@ func addScreensToProject(cli *cli, destDir, chosenTemplate string, selectedScree editedFiles = append(editedFiles, editedDirFiles...) missingFiles = append(missingFiles, missingDirFiles...) - err = handleEditedFiles(cli, editedFiles, sourceRoot, destRoot) + // Filter out screenLoader.ts since it gets regenerated by runNPMGenerate + filteredEditedFiles := filterOutScreenLoader(editedFiles) + + err = handleEditedFiles(cli, filteredEditedFiles, sourceRoot, destRoot) if err != nil { return fmt.Errorf("error during backup/overwrite: %w", err) } @@ -443,3 +446,18 @@ func updateAculConfigFile(destDir string, aculConfig *AculConfig, selectedScreen } return nil } + +// filterOutScreenLoader removes screenLoader.ts from the edited files list +// since it gets regenerated by runNPMGenerate command anyway +func filterOutScreenLoader(editedFiles []string) []string { + var filtered []string + for _, file := range editedFiles { + // Skip only the specific screenLoader.ts file that gets regenerated + normalizedPath := filepath.ToSlash(file) + if normalizedPath == "src/utils/screen/screenLoader.ts" { + continue + } + filtered = append(filtered, file) + } + return filtered +} From fe59d33cf6d624d47f08f24af0fc0204ab963a5b Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Fri, 24 Oct 2025 12:56:17 +0530 Subject: [PATCH 32/58] refactor ACUL scaffolding to consolidate post-scaffolding output --- internal/cli/acul_app_scaffolding.go | 51 +++++++++++++------------ internal/cli/acul_screen_scaffolding.go | 11 ++---- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index 15bca3d47..4195c9e4e 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -159,29 +159,7 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string) error { runNpmGenerateScreenLoader(cli, destDir) - cli.renderer.Output("") - cli.renderer.Infof("%s Project successfully created in %s!", - ansi.Bold(ansi.Green("🎉")), ansi.Bold(ansi.Cyan(fmt.Sprintf("'%s'", destDir)))) - cli.renderer.Output("") - - cli.renderer.Infof("%s Documentation:", ansi.Bold("📖")) - cli.renderer.Infof(" Explore the sample app: %s", - ansi.Blue("https://github.com/auth0-samples/auth0-acul-samples")) - cli.renderer.Output("") - - checkNodeVersion(cli) - - // Show next steps and related commands. - cli.renderer.Infof("%s Next Steps: Navigate to %s and run: 🚀", ansi.Bold(ansi.Cyan(destDir))) - cli.renderer.Infof(" 1. %s", ansi.Bold(ansi.Cyan("npm install"))) - cli.renderer.Infof(" 2. %s", ansi.Bold(ansi.Cyan("npm run build"))) - cli.renderer.Infof(" 3. %s", ansi.Bold(ansi.Cyan("npm run screen dev"))) - cli.renderer.Output("") - - showAculCommands() - - cli.renderer.Infof("%s %s: Use %s to see all available commands", - ansi.Bold("💡"), ansi.Bold("Tip"), ansi.Bold(ansi.Cyan("'auth0 acul --help'"))) + showPostScaffoldingOutput(cli, destDir, "Project successfully created") return nil } @@ -445,8 +423,28 @@ func createScreenMap(screens []Screens) map[string]Screens { return screenMap } -// showAculCommands displays available ACUL commands for user guidance. -func showAculCommands() { +// showPostScaffoldingOutput displays comprehensive post-scaffolding information including +// success message, documentation, Node version check, next steps, and available commands. +func showPostScaffoldingOutput(cli *cli, destDir, successMessage string) { + cli.renderer.Output("") + cli.renderer.Infof("%s %s in %s!", + ansi.Bold(ansi.Green("🎉")), successMessage, ansi.Bold(ansi.Cyan(fmt.Sprintf("'%s'", destDir)))) + cli.renderer.Output("") + + cli.renderer.Infof("%s Documentation:", ansi.Bold("📖")) + cli.renderer.Infof(" Explore the sample app: %s", + ansi.Blue("https://github.com/auth0-samples/auth0-acul-samples")) + cli.renderer.Output("") + + checkNodeVersion(cli) + + // Show next steps and related commands. + cli.renderer.Infof("%s Next Steps: Navigate to %s and run:", ansi.Bold("🚀"), ansi.Bold(ansi.Cyan(destDir))) + cli.renderer.Infof(" 1. %s", ansi.Bold(ansi.Cyan("npm install"))) + cli.renderer.Infof(" 2. %s", ansi.Bold(ansi.Cyan("npm run build"))) + cli.renderer.Infof(" 3. %s", ansi.Bold(ansi.Cyan("npm run screen dev"))) + cli.renderer.Output("") + fmt.Printf("%s Available Commands:\n", ansi.Bold("📋")) fmt.Printf(" %s - Add more screens to your project\n", ansi.Bold(ansi.Green("auth0 acul screen add "))) @@ -459,6 +457,9 @@ func showAculCommands() { fmt.Printf(" %s - View available screens\n", ansi.Bold(ansi.Green("auth0 acul config list"))) fmt.Println() + + fmt.Printf("%s %s: Use %s to see all available commands\n", + ansi.Bold("💡"), ansi.Bold("Tip"), ansi.Bold(ansi.Cyan("'auth0 acul --help'"))) } type AculConfig struct { diff --git a/internal/cli/acul_screen_scaffolding.go b/internal/cli/acul_screen_scaffolding.go index 73a0e7c0e..746317e2c 100644 --- a/internal/cli/acul_screen_scaffolding.go +++ b/internal/cli/acul_screen_scaffolding.go @@ -86,9 +86,7 @@ func scaffoldAddScreen(cli *cli, args []string, destDir string) error { return err } - cli.renderer.Infof(ansi.Bold(ansi.Green("Screens added successfully"))) - - showAculCommands() + showPostScaffoldingOutput(cli, destDir, "Screens added successfully") return nil } @@ -174,7 +172,7 @@ func addScreensToProject(cli *cli, destDir, chosenTemplate string, selectedScree editedFiles = append(editedFiles, editedDirFiles...) missingFiles = append(missingFiles, missingDirFiles...) - // Filter out screenLoader.ts since it gets regenerated by runNPMGenerate + // Filter out screenLoader.ts since it gets regenerated by runNpmGenerateScreenLoader. filteredEditedFiles := filterOutScreenLoader(editedFiles) err = handleEditedFiles(cli, filteredEditedFiles, sourceRoot, destRoot) @@ -447,12 +445,11 @@ func updateAculConfigFile(destDir string, aculConfig *AculConfig, selectedScreen return nil } -// filterOutScreenLoader removes screenLoader.ts from the edited files list -// since it gets regenerated by runNPMGenerate command anyway +// since it gets regenerated by runNPMGenerate command anyway. func filterOutScreenLoader(editedFiles []string) []string { var filtered []string for _, file := range editedFiles { - // Skip only the specific screenLoader.ts file that gets regenerated + // Skip only the specific screenLoader.ts file that gets regenerated. normalizedPath := filepath.ToSlash(file) if normalizedPath == "src/utils/screen/screenLoader.ts" { continue From 5664339d6001b8a87afc3aadf895ef84301f27df Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 27 Oct 2025 13:01:16 +0530 Subject: [PATCH 33/58] enhance ACUL scaffolding with template and screens flags for project initialization --- acul_config/accept-invitation.json | 8 + config/accept-invitation.json | 28 +++ docs/auth0_acul_init.md | 10 +- internal/cli/acul_app_scaffolding.go | 252 +++++++++++++++++++-------- 4 files changed, 225 insertions(+), 73 deletions(-) create mode 100644 acul_config/accept-invitation.json create mode 100644 config/accept-invitation.json diff --git a/acul_config/accept-invitation.json b/acul_config/accept-invitation.json new file mode 100644 index 000000000..79ad72b38 --- /dev/null +++ b/acul_config/accept-invitation.json @@ -0,0 +1,8 @@ +{ + "context_configuration": [], + "default_head_tags_disabled": false, + "filters": {}, + "head_tags": [], + "rendering_mode": "standard", + "use_page_template": false +} \ No newline at end of file diff --git a/config/accept-invitation.json b/config/accept-invitation.json new file mode 100644 index 000000000..0f61103d8 --- /dev/null +++ b/config/accept-invitation.json @@ -0,0 +1,28 @@ +{ + "tenant": "dev-s2xt6l5qvounptri", + "prompt": "invitation", + "screen": "accept-invitation", + "rendering_mode": "advanced", + "context_configuration": [ + "screen.texts" + ], + "default_head_tags_disabled": false, + "head_tags": [ + { + "attributes": { + "async": true, + "defer": true, + "src": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.jss" + }, + "tag": "script" + }, + { + "attributes": { + "href": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js", + "rel": "stylesheet" + }, + "tag": "link" + } + ], + "use_page_template": false +} \ No newline at end of file diff --git a/docs/auth0_acul_init.md b/docs/auth0_acul_init.md index 277da899b..792f81e41 100644 --- a/docs/auth0_acul_init.md +++ b/docs/auth0_acul_init.md @@ -18,10 +18,18 @@ auth0 acul init [flags] ``` auth0 acul init -auth0 acul init my_acul_app + auth0 acul init my_acul_app + auth0 acul init my_acul_app --template react --screens login,signup + auth0 acul init my_acul_app -t react -s login,mfa,signup ``` +## Flags + +``` + -s, --screens strings Comma-separated list of screens to include in your ACUL project. + -t, --template string Template framework to use for your ACUL project. +``` ## Inherited Flags diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index 2c5ff4d50..d6259e2dd 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -78,17 +78,32 @@ func loadManifest() (*Manifest, error) { return &manifest, nil } -var templateFlag = Flag{ - Name: "Template", - LongForm: "template", - ShortForm: "t", - Help: "Template framework to use for your ACUL project.", - IsRequired: false, -} +var ( + templateFlag = Flag{ + Name: "Template", + LongForm: "template", + ShortForm: "t", + Help: "Template framework to use for your ACUL project.", + IsRequired: false, + } + + screensFlag = Flag{ + Name: "Screens", + LongForm: "screens", + ShortForm: "s", + Help: "Comma-separated list of screens to include in your ACUL project.", + IsRequired: false, + } +) -// aculInitCmd returns the cobra.Command for project initialization. +// / aculInitCmd returns the cobra.Command for project initialization. func aculInitCmd(cli *cli) *cobra.Command { - return &cobra.Command{ + var inputs struct { + Template string + Screens []string + } + + cmd := &cobra.Command{ Use: "init", Args: cobra.MaximumNArgs(1), Short: "Generate a new ACUL project from a template", @@ -96,14 +111,24 @@ func aculInitCmd(cli *cli) *cobra.Command { This command creates a new project with your choice of framework and authentication screens (login, signup, mfa, etc.). The generated project includes all necessary configuration and boilerplate code to get started with ACUL customizations.`, Example: ` auth0 acul init -auth0 acul init my_acul_app`, + auth0 acul init my_acul_app + auth0 acul init my_acul_app --template react --screens login,signup + auth0 acul init my_acul_app -t react -s login,mfa,signup`, RunE: func(cmd *cobra.Command, args []string) error { - return runScaffold(cli, cmd, args) + return runScaffold(cli, cmd, args, &inputs) }, } + + templateFlag.RegisterString(cmd, &inputs.Template, "") + screensFlag.RegisterStringSlice(cmd, &inputs.Screens, []string{}) + + return cmd } -func runScaffold(cli *cli, cmd *cobra.Command, args []string) error { +func runScaffold(cli *cli, cmd *cobra.Command, args []string, inputs *struct { + Template string + Screens []string +}) error { if err := checkNodeInstallation(); err != nil { return err } @@ -113,12 +138,12 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string) error { return err } - chosenTemplate, err := selectTemplate(cmd, manifest) + chosenTemplate, err := selectTemplate(cmd, manifest, inputs.Template) if err != nil { return err } - selectedScreens, err := selectScreens(manifest.Templates[chosenTemplate].Screens) + selectedScreens, err := selectScreens(cli, manifest.Templates[chosenTemplate].Screens, inputs.Screens) if err != nil { return err } @@ -159,17 +184,12 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string) error { runNpmGenerateScreenLoader(cli, destDir) - fmt.Printf("\nProject successfully created in '%s'!\n\n", destDir) - - fmt.Println("\n📖 Documentation:") - fmt.Println("Explore the sample app: https://github.com/auth0-samples/auth0-acul-samples") - - checkNodeVersion(cli) + showPostScaffoldingOutput(cli, destDir, "Project successfully created") return nil } -func selectTemplate(cmd *cobra.Command, manifest *Manifest) (string, error) { +func selectTemplate(cmd *cobra.Command, manifest *Manifest, providedTemplate string) (string, error) { var templateNames []string nameToKey := make(map[string]string) @@ -178,6 +198,17 @@ func selectTemplate(cmd *cobra.Command, manifest *Manifest) (string, error) { nameToKey[template.Name] = key } + // If template provided via flag, validate it. + if providedTemplate != "" { + for key, template := range manifest.Templates { + if template.Name == providedTemplate || key == providedTemplate { + return key, nil + } + } + return "", fmt.Errorf("invalid template '%s'. Available templates: %s", + providedTemplate, strings.Join(templateNames, ", ")) + } + var chosenTemplateName string err := templateFlag.Select(cmd, &chosenTemplateName, templateNames, nil) if err != nil { @@ -186,13 +217,60 @@ func selectTemplate(cmd *cobra.Command, manifest *Manifest) (string, error) { return nameToKey[chosenTemplateName], nil } -func selectScreens(screens []Screens) ([]string, error) { - var screenOptions []string +func selectScreens(cli *cli, screens []Screens, providedScreens []string) ([]string, error) { + var availableScreenIDs []string for _, s := range screens { - screenOptions = append(screenOptions, s.ID) + availableScreenIDs = append(availableScreenIDs, s.ID) + } + + // If screens provided via flag, validate them. + if len(providedScreens) > 0 { + var validScreens []string + var invalidScreens []string + + for _, providedScreen := range providedScreens { + // Skip empty strings. + if strings.TrimSpace(providedScreen) == "" { + continue + } + + found := false + for _, availableScreen := range availableScreenIDs { + if providedScreen == availableScreen { + validScreens = append(validScreens, providedScreen) + found = true + break + } + } + if !found { + invalidScreens = append(invalidScreens, providedScreen) + } + } + + if len(invalidScreens) > 0 { + cli.renderer.Warnf("%s The following screens are not supported for the chosen template: %s", + ansi.Bold(ansi.Yellow("⚠️")), + ansi.Bold(ansi.Red(strings.Join(invalidScreens, ", ")))) + cli.renderer.Infof("%s %s", + ansi.Bold("Available screens:"), + ansi.Bold(ansi.Cyan(strings.Join(availableScreenIDs, ", ")))) + cli.renderer.Infof("%s %s", + ansi.Bold(ansi.Blue("Note:")), + ansi.Faint("We're planning to support all screens in the future.")) + } + + if len(validScreens) == 0 { + cli.renderer.Warnf("%s %s", + ansi.Bold(ansi.Yellow("⚠️")), + ansi.Bold("None of the provided screens are valid for this template.")) + } else { + return validScreens, nil + } } + + // If no screens provided via flag or no valid screens, prompt for multi-select. var selectedScreens []string - err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenOptions...) + err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, availableScreenIDs...) if len(selectedScreens) == 0 { return nil, fmt.Errorf("at least one screen must be selected") @@ -226,24 +304,19 @@ func downloadAndUnzipSampleRepo() (string, error) { } func copyTemplateBaseDirs(cli *cli, baseDirs []string, chosenTemplate, tempUnzipDir, destDir string) error { - sourcePathPrefix := "auth0-acul-samples-monorepo-sample/" + chosenTemplate - for _, dir := range baseDirs { - // TODO: Remove hardcoding of removing the template - instead ensure to remove the template name in sourcePathPrefix. - relPath, err := filepath.Rel(chosenTemplate, dir) - if err != nil { - continue - } - - srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, relPath) - destPath := filepath.Join(destDir, relPath) - - if _, err = os.Stat(srcPath); os.IsNotExist(err) { - cli.renderer.Warnf("Warning: Source directory does not exist: %s", srcPath) + sourcePathPrefix := filepath.Join("auth0-acul-samples-monorepo-sample", chosenTemplate) + for _, dirPath := range baseDirs { + srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, dirPath) + destPath := filepath.Join(destDir, dirPath) + + if _, err := os.Stat(srcPath); os.IsNotExist(err) { + cli.renderer.Warnf("%s Source directory does not exist: %s", + ansi.Bold(ansi.Yellow("⚠️")), ansi.Faint(srcPath)) continue } if err := copyDir(srcPath, destPath); err != nil { - return fmt.Errorf("error copying directory %s: %w", dir, err) + return fmt.Errorf("error copying directory %s: %w", dirPath, err) } } @@ -251,30 +324,27 @@ func copyTemplateBaseDirs(cli *cli, baseDirs []string, chosenTemplate, tempUnzip } func copyProjectTemplateFiles(cli *cli, baseFiles []string, chosenTemplate, tempUnzipDir, destDir string) error { - sourcePathPrefix := "auth0-acul-samples-monorepo-sample/" + chosenTemplate - for _, baseFile := range baseFiles { - // TODO: Remove hardcoding of removing the template - instead ensure to remove the template name in sourcePathPrefix. - relPath, err := filepath.Rel(chosenTemplate, baseFile) - if err != nil { - continue - } + sourcePathPrefix := filepath.Join("auth0-acul-samples-monorepo-sample", chosenTemplate) - srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, relPath) - destPath := filepath.Join(destDir, relPath) + for _, filePath := range baseFiles { + srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, filePath) + destPath := filepath.Join(destDir, filePath) - if _, err = os.Stat(srcPath); os.IsNotExist(err) { - cli.renderer.Warnf("Warning: Source file does not exist: %s", srcPath) + if _, err := os.Stat(srcPath); os.IsNotExist(err) { + cli.renderer.Warnf("%s Source file does not exist: %s", + ansi.Bold(ansi.Yellow("⚠️")), ansi.Faint(srcPath)) continue } parentDir := filepath.Dir(destPath) if err := os.MkdirAll(parentDir, 0755); err != nil { - cli.renderer.Warnf("Error creating parent directory for %s: %v", baseFile, err) + cli.renderer.Warnf("%s Error creating parent directory for %s: %v", + ansi.Bold(ansi.Red("❌")), ansi.Bold(filePath), err) continue } if err := copyFile(srcPath, destPath); err != nil { - return fmt.Errorf("error copying file %s: %w", baseFile, err) + return fmt.Errorf("error copying file %s: %w", filePath, err) } } @@ -287,22 +357,19 @@ func copyProjectScreens(cli *cli, screens []Screens, selectedScreens []string, c for _, s := range selectedScreens { screen := screenInfo[s] - relPath, err := filepath.Rel(chosenTemplate, screen.Path) - if err != nil { - continue - } + srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, screen.Path) + destPath := filepath.Join(destDir, screen.Path) - srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, relPath) - destPath := filepath.Join(destDir, relPath) - - if _, err = os.Stat(srcPath); os.IsNotExist(err) { - cli.renderer.Warnf("Warning: Source directory does not exist: %s", srcPath) + if _, err := os.Stat(srcPath); os.IsNotExist(err) { + cli.renderer.Warnf("%s Source directory does not exist: %s", + ansi.Bold(ansi.Yellow("⚠️")), ansi.Faint(srcPath)) continue } parentDir := filepath.Dir(destPath) if err := os.MkdirAll(parentDir, 0755); err != nil { - cli.renderer.Warnf("Error creating parent directory for %s: %v", screen.Path, err) + cli.renderer.Warnf("%s Error creating parent directory for %s: %v", + ansi.Bold(ansi.Red("❌")), ansi.Bold(screen.Path), err) continue } @@ -423,6 +490,44 @@ func createScreenMap(screens []Screens) map[string]Screens { return screenMap } +// showPostScaffoldingOutput displays comprehensive post-scaffolding information including +// success message, documentation, Node version check, next steps, and available commands. +func showPostScaffoldingOutput(cli *cli, destDir, successMessage string) { + cli.renderer.Output("") + cli.renderer.Infof("%s %s in %s!", + ansi.Bold(ansi.Green("🎉")), successMessage, ansi.Bold(ansi.Cyan(fmt.Sprintf("'%s'", destDir)))) + cli.renderer.Output("") + + cli.renderer.Infof("📖 Explore the sample app: %s", + ansi.Blue("https://github.com/auth0-samples/auth0-acul-samples")) + cli.renderer.Output("") + + checkNodeVersion(cli) + + // Show next steps and related commands. + cli.renderer.Infof("%s Next Steps: Navigate to %s and run:", ansi.Bold("🚀"), ansi.Bold(ansi.Cyan(destDir))) + cli.renderer.Infof(" 1. %s", ansi.Bold(ansi.Cyan("npm install"))) + cli.renderer.Infof(" 2. %s", ansi.Bold(ansi.Cyan("npm run build"))) + cli.renderer.Infof(" 3. %s", ansi.Bold(ansi.Cyan("npm run screen dev"))) + cli.renderer.Output("") + + fmt.Printf("%s Available Commands:\n", ansi.Bold("📋")) + fmt.Printf(" %s - Add more screens to your project\n", + ansi.Bold(ansi.Green("auth0 acul screen add "))) + fmt.Printf(" %s - Generate a stub config file\n", + ansi.Bold(ansi.Green("auth0 acul config generate "))) + fmt.Printf(" %s - Download current settings\n", + ansi.Bold(ansi.Green("auth0 acul config get "))) + fmt.Printf(" %s - Upload customizations\n", + ansi.Bold(ansi.Green("auth0 acul config set "))) + fmt.Printf(" %s - View available screens\n", + ansi.Bold(ansi.Green("auth0 acul config list"))) + fmt.Println() + + fmt.Printf("%s %s: Use %s to see all available commands\n", + ansi.Bold("💡"), ansi.Bold("Tip"), ansi.Bold(ansi.Cyan("'auth0 acul --help'"))) +} + type AculConfig struct { ChosenTemplate string `json:"chosen_template"` Screens []string `json:"screens"` @@ -457,19 +562,21 @@ func checkNodeVersion(cli *cli) { } if major, _ := strconv.Atoi(matches[1]); major < 22 { - fmt.Printf( - "⚠️ Node %s detected. This project requires Node v22 or higher.\n"+ - " Please upgrade to Node v22+ to run the sample app and build assets successfully.\n", - version, + fmt.Println( + ansi.Yellow(fmt.Sprintf( + "⚠️ Node %s detected. This project requires Node v22 or higher.\n"+ + " Please upgrade to Node v22+ to run the sample app and build assets successfully.\n", + version, + )), ) + + cli.renderer.Output("") } } // runNpmGenerateScreenLoader runs `npm run generate:screenLoader` in the given directory. // Prints errors or warnings directly; silent if successful with no issues. func runNpmGenerateScreenLoader(cli *cli, destDir string) { - fmt.Println(ansi.Blue("🔄 Generating screen loader...")) - cmd := exec.Command("npm", "run", "generate:screenLoader") cmd.Dir = destDir @@ -491,10 +598,11 @@ func runNpmGenerateScreenLoader(cli *cli, destDir string) { ansi.Bold(ansi.Cyan(fmt.Sprintf("cd %s && npm run generate:screenLoader", destDir))), ansi.Faint(fmt.Sprintf("%s/src/utils/screen/screenLoader.ts", destDir)), ) - return - } - if len(summary) > 0 { - fmt.Println(summary) + if len(summary) > 0 { + fmt.Println(summary) + } + + return } } From 55007a3e1e39b480db0f98fc2be8de1dbbe19bc9 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Tue, 28 Oct 2025 09:16:51 +0530 Subject: [PATCH 34/58] Initial commit --- internal/cli/acul_dev_connected.go | 240 ++++++++++++++++++++++ internal/cli/universal_login_customize.go | 229 --------------------- 2 files changed, 240 insertions(+), 229 deletions(-) create mode 100644 internal/cli/acul_dev_connected.go diff --git a/internal/cli/acul_dev_connected.go b/internal/cli/acul_dev_connected.go new file mode 100644 index 000000000..676dd15fa --- /dev/null +++ b/internal/cli/acul_dev_connected.go @@ -0,0 +1,240 @@ +package cli + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "reflect" + "strings" + "sync" + "time" + + "github.com/auth0/go-auth0/management" + "github.com/fsnotify/fsnotify" + "github.com/spf13/cobra" +) + +var ( + watchFolder = Flag{ + Name: "Watch Folder", + LongForm: "watch-folder", + ShortForm: "w", + Help: "Folder to watch for new builds. CLI will watch for changes in the folder and automatically update the assets.", + IsRequired: true, + } + + assetURL = Flag{ + Name: "Assets URL", + LongForm: "assets-url", + ShortForm: "u", + Help: "Base URL for serving dist assets (e.g., http://localhost:5173).", + IsRequired: true, + } + + screensFlag1 = Flag{ + Name: "screen", + LongForm: "screens", + ShortForm: "s", + Help: "watching screens", + IsRequired: true, + AlwaysPrompt: true, + } +) + +func newUpdateAssetsCmd(cli *cli) *cobra.Command { + var watchFolders, assetsURL string + var screens []string + + cmd := &cobra.Command{ + Use: "watch-assets", + Short: "Watch the dist folder and patch screen assets. You can watch all screens or one or more specific screens.", + Example: ` auth0 universal-login watch-assets --screens login-id,login,signup,email-identifier-challenge,login-passwordless-email-code --watch-folder "/dist" --assets-url "http://localhost:8080" + auth0 ul watch-assets --screens all -w "/dist" -u "http://localhost:8080" + auth0 ul watch-assets --screen login-id --watch-folder "/dist"" --assets-url "http://localhost:8080" + auth0 ul switch -p login-id -s login-id -r standard`, + RunE: func(cmd *cobra.Command, args []string) error { + return watchAndPatch(context.Background(), cli, assetsURL, watchFolders, screens) + }, + } + + screensFlag1.RegisterStringSlice(cmd, &screens, nil) + watchFolder.RegisterString(cmd, &watchFolders, "") + assetURL.RegisterString(cmd, &assetsURL, "") + + return cmd +} + +func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screenDirs []string) error { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + defer watcher.Close() + + distAssetsPath := filepath.Join(distPath, "assets") + var screensToWatch []string + + if len(screenDirs) == 1 && screenDirs[0] == "all" { + dirs, err := os.ReadDir(distAssetsPath) + if err != nil { + return fmt.Errorf("failed to read assets dir: %w", err) + } + + for _, d := range dirs { + if d.IsDir() && d.Name() != "shared" { + screensToWatch = append(screensToWatch, d.Name()) + } + } + } else { + for _, screen := range screenDirs { + path := filepath.Join(distAssetsPath, screen) + info, err := os.Stat(path) + if err != nil { + log.Printf("Screen directory %q not found in dist/assets: %v", screen, err) + continue + } + if !info.IsDir() { + log.Printf("Screen path %q exists but is not a directory", path) + continue + } + screensToWatch = append(screensToWatch, screen) + } + } + + if err := watcher.Add(distPath); err != nil { + log.Printf("Failed to watch %q: %v", distPath, err) + } else { + log.Printf("👀 Watching: %d screen(s): %v", len(screensToWatch), screensToWatch) + } + + const debounceWindow = 5 * time.Second + var lastProcessTime time.Time + lastHeadTags := make(map[string][]interface{}) + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return nil + } + + if strings.HasSuffix(event.Name, "assets") && event.Op&(fsnotify.Create) != 0 { + now := time.Now() + if now.Sub(lastProcessTime) < debounceWindow { + log.Println("⏱️ Ignoring event due to debounce window") + continue + } + lastProcessTime = now + + time.Sleep(500 * time.Millisecond) // short delay to let writes settle + log.Println("📦 Change detected in assets folder. Rebuilding and patching...") + + var wg sync.WaitGroup + errChan := make(chan error, len(screensToWatch)) + + for _, screen := range screensToWatch { + wg.Add(1) + + go func(screen string) { + defer wg.Done() + + headTags, err := buildHeadTagsFromDirs(filepath.Dir(distAssetsPath), assetsURL, screen) + if err != nil { + errChan <- fmt.Errorf("failed to build headTags for %s: %w", screen, err) + return + } + + if reflect.DeepEqual(lastHeadTags[screen], headTags) { + log.Printf("🔁 Skipping patch for '%s' — headTags unchanged", screen) + return + } + + log.Printf("📦 Detected changes for screen '%s'", screen) + lastHeadTags[screen] = headTags + + var settings = &management.PromptRendering{ + HeadTags: headTags, + } + + if err = cli.api.Prompt.UpdateRendering(ctx, management.PromptType(ScreenPromptMap[screen]), management.ScreenName(screen), settings); err != nil { + errChan <- fmt.Errorf("failed to patch settings for %s: %w", screen, err) + return + } + + log.Printf("✅ Successfully patched screen '%s'", screen) + }(screen) + } + + wg.Wait() + close(errChan) + + for err = range errChan { + log.Println(err) + } + } + + case err = <-watcher.Errors: + log.Println("Watcher error: ", err) + + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func buildHeadTagsFromDirs(distPath, assetsURL, screen string) ([]interface{}, error) { + var tags []interface{} + screenPath := filepath.Join(distPath, "assets", screen) + sharedPath := filepath.Join(distPath, "assets", "shared") + mainPath := filepath.Join(distPath, "assets") + + sources := []string{sharedPath, screenPath, mainPath} + + for _, dir := range sources { + entries, err := os.ReadDir(dir) + if err != nil { + continue // skip on error + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + subDir := filepath.Base(dir) + if subDir == "assets" { + subDir = "" // root-level main-*.js + } + src := fmt.Sprintf("%s/assets/%s%s", assetsURL, subDir, name) + if subDir != "" { + src = fmt.Sprintf("%s/assets/%s/%s", assetsURL, subDir, name) + } + + ext := filepath.Ext(name) + + switch ext { + case ".js": + tags = append(tags, map[string]interface{}{ + "tag": "script", + "attributes": map[string]interface{}{ + "src": src, + "defer": true, + "type": "module", + }, + }) + case ".css": + tags = append(tags, map[string]interface{}{ + "tag": "link", + "attributes": map[string]interface{}{ + "href": src, + "rel": "stylesheet", + }, + }) + } + } + } + + return tags, nil +} diff --git a/internal/cli/universal_login_customize.go b/internal/cli/universal_login_customize.go index 01094a6d0..1107b10c4 100644 --- a/internal/cli/universal_login_customize.go +++ b/internal/cli/universal_login_customize.go @@ -6,20 +6,14 @@ import ( "encoding/json" "fmt" "io/fs" - "log" "net" "net/http" "net/url" - "os" - "path/filepath" - "reflect" "strings" - "sync" "time" "github.com/auth0/go-auth0/management" - "github.com/fsnotify/fsnotify" "github.com/gorilla/websocket" "github.com/pkg/browser" "github.com/spf13/cobra" @@ -50,33 +44,6 @@ var ( ErrNoChangesDetected = fmt.Errorf("no changes detected") ) -var ( - watchFolder = Flag{ - Name: "Watch Folder", - LongForm: "watch-folder", - ShortForm: "w", - Help: "Folder to watch for new builds. CLI will watch for changes in the folder and automatically update the assets.", - IsRequired: true, - } - - assetURL = Flag{ - Name: "Assets URL", - LongForm: "assets-url", - ShortForm: "u", - Help: "Base URL for serving dist assets (e.g., http://localhost:5173).", - IsRequired: true, - } - - screens1 = Flag{ - Name: "screen", - LongForm: "screens", - ShortForm: "s", - Help: "watching screens", - IsRequired: true, - AlwaysPrompt: true, - } -) - var allowedPromptsWithPartials = []management.PromptType{ management.PromptSignup, management.PromptSignupID, @@ -855,199 +822,3 @@ func saveUniversalLoginBrandingData(ctx context.Context, api *auth0.API, data *u return group.Wait() } - -func newUpdateAssetsCmd(cli *cli) *cobra.Command { - var watchFolders, assetsURL string - var screens []string - - cmd := &cobra.Command{ - Use: "watch-assets", - Short: "Watch the dist folder and patch screen assets. You can watch all screens or one or more specific screens.", - Example: ` auth0 universal-login watch-assets --screens login-id,login,signup,email-identifier-challenge,login-passwordless-email-code --watch-folder "/dist" --assets-url "http://localhost:8080" - auth0 ul watch-assets --screens all -w "/dist" -u "http://localhost:8080" - auth0 ul watch-assets --screen login-id --watch-folder "/dist"" --assets-url "http://localhost:8080" - auth0 ul switch -p login-id -s login-id -r standard`, - RunE: func(cmd *cobra.Command, args []string) error { - return watchAndPatch(context.Background(), cli, assetsURL, watchFolders, screens) - }, - } - - screens1.RegisterStringSlice(cmd, &screens, nil) - watchFolder.RegisterString(cmd, &watchFolders, "") - assetURL.RegisterString(cmd, &assetsURL, "") - - return cmd -} - -func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screenDirs []string) error { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - defer watcher.Close() - - distAssetsPath := filepath.Join(distPath, "assets") - var screensToWatch []string - - if len(screenDirs) == 1 && screenDirs[0] == "all" { - dirs, err := os.ReadDir(distAssetsPath) - if err != nil { - return fmt.Errorf("failed to read assets dir: %w", err) - } - - for _, d := range dirs { - if d.IsDir() && d.Name() != "shared" { - screensToWatch = append(screensToWatch, d.Name()) - } - } - } else { - for _, screen := range screenDirs { - path := filepath.Join(distAssetsPath, screen) - info, err := os.Stat(path) - if err != nil { - log.Printf("Screen directory %q not found in dist/assets: %v", screen, err) - continue - } - if !info.IsDir() { - log.Printf("Screen path %q exists but is not a directory", path) - continue - } - screensToWatch = append(screensToWatch, screen) - } - } - - if err := watcher.Add(distPath); err != nil { - log.Printf("Failed to watch %q: %v", distPath, err) - } else { - log.Printf("👀 Watching: %d screen(s): %v", len(screensToWatch), screensToWatch) - } - - const debounceWindow = 5 * time.Second - var lastProcessTime time.Time - lastHeadTags := make(map[string][]interface{}) - - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return nil - } - - if strings.HasSuffix(event.Name, "assets") && event.Op&(fsnotify.Create) != 0 { - now := time.Now() - if now.Sub(lastProcessTime) < debounceWindow { - log.Println("⏱️ Ignoring event due to debounce window") - continue - } - lastProcessTime = now - - time.Sleep(500 * time.Millisecond) // short delay to let writes settle - log.Println("📦 Change detected in assets folder. Rebuilding and patching...") - - var wg sync.WaitGroup - errChan := make(chan error, len(screensToWatch)) - - for _, screen := range screensToWatch { - wg.Add(1) - - go func(screen string) { - defer wg.Done() - - headTags, err := buildHeadTagsFromDirs(filepath.Dir(distAssetsPath), assetsURL, screen) - if err != nil { - errChan <- fmt.Errorf("failed to build headTags for %s: %w", screen, err) - return - } - - if reflect.DeepEqual(lastHeadTags[screen], headTags) { - log.Printf("🔁 Skipping patch for '%s' — headTags unchanged", screen) - return - } - - log.Printf("📦 Detected changes for screen '%s'", screen) - lastHeadTags[screen] = headTags - - var settings = &management.PromptRendering{ - HeadTags: headTags, - } - - if err = cli.api.Prompt.UpdateRendering(ctx, management.PromptType(ScreenPromptMap[screen]), management.ScreenName(screen), settings); err != nil { - errChan <- fmt.Errorf("failed to patch settings for %s: %w", screen, err) - return - } - - log.Printf("✅ Successfully patched screen '%s'", screen) - }(screen) - } - - wg.Wait() - close(errChan) - - for err = range errChan { - log.Println(err) - } - } - - case err = <-watcher.Errors: - log.Println("Watcher error: ", err) - - case <-ctx.Done(): - return ctx.Err() - } - } -} - -func buildHeadTagsFromDirs(distPath, assetsURL, screen string) ([]interface{}, error) { - var tags []interface{} - screenPath := filepath.Join(distPath, "assets", screen) - sharedPath := filepath.Join(distPath, "assets", "shared") - mainPath := filepath.Join(distPath, "assets") - - sources := []string{sharedPath, screenPath, mainPath} - - for _, dir := range sources { - entries, err := os.ReadDir(dir) - if err != nil { - continue // skip on error - } - - for _, entry := range entries { - if entry.IsDir() { - continue - } - name := entry.Name() - subDir := filepath.Base(dir) - if subDir == "assets" { - subDir = "" // root-level main-*.js - } - src := fmt.Sprintf("%s/assets/%s%s", assetsURL, subDir, name) - if subDir != "" { - src = fmt.Sprintf("%s/assets/%s/%s", assetsURL, subDir, name) - } - - ext := filepath.Ext(name) - - switch ext { - case ".js": - tags = append(tags, map[string]interface{}{ - "tag": "script", - "attributes": map[string]interface{}{ - "src": src, - "defer": true, - "type": "module", - }, - }) - case ".css": - tags = append(tags, map[string]interface{}{ - "tag": "link", - "attributes": map[string]interface{}{ - "href": src, - "rel": "stylesheet", - }, - }) - } - } - } - - return tags, nil -} From 10a7a6c59d18224ad6e8ac0f8ae3726ace7cdc9f Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Wed, 29 Oct 2025 14:40:17 +0530 Subject: [PATCH 35/58] Add 'dev' command for connected mode with asset watching --- docs/auth0_acul.md | 1 + docs/auth0_acul_dev.md | 59 ++++++ docs/auth0_acul_init.md | 1 + docs/auth0_universal-login.md | 1 - docs/auth0_universal-login_customize.md | 1 - docs/auth0_universal-login_show.md | 1 - docs/auth0_universal-login_switch.md | 0 docs/auth0_universal-login_update.md | 1 - docs/auth0_universal-login_watch-assets.md | 54 ----- internal/cli/acul.go | 1 + internal/cli/acul_dev_connected.go | 230 ++++++++++++++------- internal/cli/universal_login.go | 1 - 12 files changed, 216 insertions(+), 135 deletions(-) create mode 100644 docs/auth0_acul_dev.md delete mode 100644 docs/auth0_universal-login_switch.md delete mode 100644 docs/auth0_universal-login_watch-assets.md diff --git a/docs/auth0_acul.md b/docs/auth0_acul.md index 00bc2bf03..c45bb9e92 100644 --- a/docs/auth0_acul.md +++ b/docs/auth0_acul.md @@ -10,6 +10,7 @@ Customize the Universal Login experience. This requires a custom domain to be co ## Commands - [auth0 acul config](auth0_acul_config.md) - Configure Advanced Customizations for Universal Login screens. +- [auth0 acul dev](auth0_acul_dev.md) - Start development mode for ACUL project with automatic building and asset watching. - [auth0 acul init](auth0_acul_init.md) - Generate a new ACUL project from a template - [auth0 acul screen](auth0_acul_screen.md) - Manage individual screens for Advanced Customizations for Universal Login. diff --git a/docs/auth0_acul_dev.md b/docs/auth0_acul_dev.md new file mode 100644 index 000000000..fd911255b --- /dev/null +++ b/docs/auth0_acul_dev.md @@ -0,0 +1,59 @@ +--- +layout: default +parent: auth0 acul +has_toc: false +--- +# auth0 acul dev + +Start development mode for an ACUL project. This command: +- Runs 'npm run build' to build the project initially +- Watches the dist directory for asset changes +- Automatically patches screen assets when new builds are created +- Supports both single screen development and all screens + +The project directory must contain package.json with a build script. +You need to run your own build process (e.g., npm run build, npm run screen ) +to generate new assets that will be automatically detected and patched. + +## Usage +``` +auth0 acul dev [flags] +``` + +## Examples + +``` + auth0 acul dev + auth0 acul dev --dir ./my_acul_project + auth0 acul dev --screen login-id --port 3000 + auth0 acul dev -d ./project -s login-id -p 8080 +``` + + +## Flags + +``` + -d, --dir string Path to the ACUL project directory (must contain package.json). + -p, --port string Port for the local development server (default: 8080). (default "8080") + -s, --screen strings Specific screen to develop and watch. If not provided, will watch all screens in the dist/assets folder. +``` + + +## Inherited Flags + +``` + --debug Enable debug mode. + --no-color Disable colors. + --no-input Disable interactivity. + --tenant string Specific tenant to use. +``` + + +## Related Commands + +- [auth0 acul config](auth0_acul_config.md) - Configure Advanced Customizations for Universal Login screens. +- [auth0 acul dev](auth0_acul_dev.md) - Start development mode for ACUL project with automatic building and asset watching. +- [auth0 acul init](auth0_acul_init.md) - Generate a new ACUL project from a template +- [auth0 acul screen](auth0_acul_screen.md) - Manage individual screens for Advanced Customizations for Universal Login. + + diff --git a/docs/auth0_acul_init.md b/docs/auth0_acul_init.md index 905c9473f..acb450cf7 100644 --- a/docs/auth0_acul_init.md +++ b/docs/auth0_acul_init.md @@ -45,6 +45,7 @@ auth0 acul init [flags] ## Related Commands - [auth0 acul config](auth0_acul_config.md) - Configure Advanced Customizations for Universal Login screens. +- [auth0 acul dev](auth0_acul_dev.md) - Start development mode for ACUL project with automatic building and asset watching. - [auth0 acul init](auth0_acul_init.md) - Generate a new ACUL project from a template - [auth0 acul screen](auth0_acul_screen.md) - Manage individual screens for Advanced Customizations for Universal Login. diff --git a/docs/auth0_universal-login.md b/docs/auth0_universal-login.md index 8f50b6278..45468c0a3 100644 --- a/docs/auth0_universal-login.md +++ b/docs/auth0_universal-login.md @@ -14,5 +14,4 @@ Manage a consistent, branded Universal Login experience that can handle all of y - [auth0 universal-login show](auth0_universal-login_show.md) - Display the custom branding settings for Universal Login - [auth0 universal-login templates](auth0_universal-login_templates.md) - Manage custom Universal Login templates - [auth0 universal-login update](auth0_universal-login_update.md) - Update the custom branding settings for Universal Login -- [auth0 universal-login watch-assets](auth0_universal-login_watch-assets.md) - Watch dist folder and patch screen assets. We can watch for all or 1 or more screens. diff --git a/docs/auth0_universal-login_customize.md b/docs/auth0_universal-login_customize.md index ee091881c..727ad77ed 100644 --- a/docs/auth0_universal-login_customize.md +++ b/docs/auth0_universal-login_customize.md @@ -39,6 +39,5 @@ auth0 universal-login customize [flags] - [auth0 universal-login show](auth0_universal-login_show.md) - Display the custom branding settings for Universal Login - [auth0 universal-login templates](auth0_universal-login_templates.md) - Manage custom Universal Login templates - [auth0 universal-login update](auth0_universal-login_update.md) - Update the custom branding settings for Universal Login -- [auth0 universal-login watch-assets](auth0_universal-login_watch-assets.md) - Watch dist folder and patch screen assets. We can watch for all or 1 or more screens. diff --git a/docs/auth0_universal-login_show.md b/docs/auth0_universal-login_show.md index db3746d34..d713dc2cb 100644 --- a/docs/auth0_universal-login_show.md +++ b/docs/auth0_universal-login_show.md @@ -47,6 +47,5 @@ auth0 universal-login show [flags] - [auth0 universal-login show](auth0_universal-login_show.md) - Display the custom branding settings for Universal Login - [auth0 universal-login templates](auth0_universal-login_templates.md) - Manage custom Universal Login templates - [auth0 universal-login update](auth0_universal-login_update.md) - Update the custom branding settings for Universal Login -- [auth0 universal-login watch-assets](auth0_universal-login_watch-assets.md) - Watch dist folder and patch screen assets. We can watch for all or 1 or more screens. diff --git a/docs/auth0_universal-login_switch.md b/docs/auth0_universal-login_switch.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/auth0_universal-login_update.md b/docs/auth0_universal-login_update.md index ced4981a2..9586bc5d7 100644 --- a/docs/auth0_universal-login_update.md +++ b/docs/auth0_universal-login_update.md @@ -57,6 +57,5 @@ auth0 universal-login update [flags] - [auth0 universal-login show](auth0_universal-login_show.md) - Display the custom branding settings for Universal Login - [auth0 universal-login templates](auth0_universal-login_templates.md) - Manage custom Universal Login templates - [auth0 universal-login update](auth0_universal-login_update.md) - Update the custom branding settings for Universal Login -- [auth0 universal-login watch-assets](auth0_universal-login_watch-assets.md) - Watch dist folder and patch screen assets. We can watch for all or 1 or more screens. diff --git a/docs/auth0_universal-login_watch-assets.md b/docs/auth0_universal-login_watch-assets.md deleted file mode 100644 index 3c155319e..000000000 --- a/docs/auth0_universal-login_watch-assets.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -layout: default -parent: auth0 universal-login -has_toc: false ---- -# auth0 universal-login watch-assets - - - -## Usage -``` -auth0 universal-login watch-assets [flags] -``` - -## Examples - -``` - auth0 universal-login watch-assets --screens login-id,login,signup,email-identifier-challenge,login-passwordless-email-code --watch-folder "/dist" --assets-url "http://localhost:8080" - auth0 ul watch-assets --screens all -w "/dist" -u "http://localhost:8080" - auth0 ul watch-assets --screen login-id --watch-folder "/dist"" --assets-url "http://localhost:8080" - auth0 ul switch -p login-id -s login-id -r standard -``` - - -## Flags - -``` - -u, --assets-url string Base URL for serving dist assets (e.g., http://localhost:5173). - -s, --screens strings watching screens - -w, --watch-folder string Folder to watch for new builds. CLI will watch for changes in the folder and automatically update the assets. -``` - - -## Inherited Flags - -``` - --debug Enable debug mode. - --no-color Disable colors. - --no-input Disable interactivity. - --tenant string Specific tenant to use. -``` - - -## Related Commands - -- [auth0 universal-login customize](auth0_universal-login_customize.md) - Customize the Universal Login experience for the standard or advanced mode -- [auth0 universal-login prompts](auth0_universal-login_prompts.md) - Manage custom text for prompts -- [auth0 universal-login show](auth0_universal-login_show.md) - Display the custom branding settings for Universal Login -- [auth0 universal-login switch](auth0_universal-login_switch.md) - Switch the rendering mode for Universal Login -- [auth0 universal-login templates](auth0_universal-login_templates.md) - Manage custom Universal Login templates -- [auth0 universal-login update](auth0_universal-login_update.md) - Update the custom branding settings for Universal Login -- [auth0 universal-login watch-assets](auth0_universal-login_watch-assets.md) - Watch dist folder and patch screen assets. We can watch for all or 1 or more screens. - - diff --git a/internal/cli/acul.go b/internal/cli/acul.go index ceac4842c..100412001 100644 --- a/internal/cli/acul.go +++ b/internal/cli/acul.go @@ -12,6 +12,7 @@ func aculCmd(cli *cli) *cobra.Command { cmd.AddCommand(aculConfigureCmd(cli)) cmd.AddCommand(aculInitCmd(cli)) cmd.AddCommand(aculScreenCmd(cli)) + cmd.AddCommand(aculDevCmd(cli)) return cmd } diff --git a/internal/cli/acul_dev_connected.go b/internal/cli/acul_dev_connected.go index 676dd15fa..fd8dbc77c 100644 --- a/internal/cli/acul_dev_connected.go +++ b/internal/cli/acul_dev_connected.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "os/exec" "path/filepath" "reflect" "strings" @@ -17,55 +18,130 @@ import ( ) var ( - watchFolder = Flag{ - Name: "Watch Folder", - LongForm: "watch-folder", - ShortForm: "w", - Help: "Folder to watch for new builds. CLI will watch for changes in the folder and automatically update the assets.", - IsRequired: true, + // New flags for acul dev command + projectDirFlag = Flag{ + Name: "Project Directory", + LongForm: "dir", + ShortForm: "d", + Help: "Path to the ACUL project directory (must contain package.json).", + IsRequired: false, } - assetURL = Flag{ - Name: "Assets URL", - LongForm: "assets-url", - ShortForm: "u", - Help: "Base URL for serving dist assets (e.g., http://localhost:5173).", - IsRequired: true, + screenDevFlag = Flag{ + Name: "Screen", + LongForm: "screen", + ShortForm: "s", + Help: "Specific screen to develop and watch. If not provided, will watch all screens in the dist/assets folder.", + IsRequired: false, + AlwaysPrompt: false, } - screensFlag1 = Flag{ - Name: "screen", - LongForm: "screens", - ShortForm: "s", - Help: "watching screens", - IsRequired: true, - AlwaysPrompt: true, + portFlag = Flag{ + Name: "Port", + LongForm: "port", + ShortForm: "p", + Help: "Port for the local development server (default: 8080).", + IsRequired: false, } ) -func newUpdateAssetsCmd(cli *cli) *cobra.Command { - var watchFolders, assetsURL string - var screens []string +func aculDevCmd(cli *cli) *cobra.Command { + var projectDir, port string + var screenDirs []string cmd := &cobra.Command{ - Use: "watch-assets", - Short: "Watch the dist folder and patch screen assets. You can watch all screens or one or more specific screens.", - Example: ` auth0 universal-login watch-assets --screens login-id,login,signup,email-identifier-challenge,login-passwordless-email-code --watch-folder "/dist" --assets-url "http://localhost:8080" - auth0 ul watch-assets --screens all -w "/dist" -u "http://localhost:8080" - auth0 ul watch-assets --screen login-id --watch-folder "/dist"" --assets-url "http://localhost:8080" - auth0 ul switch -p login-id -s login-id -r standard`, + Use: "dev", + Short: "Start development mode for ACUL project with automatic building and asset watching.", + Long: `Start development mode for an ACUL project. This command: +- Runs 'npm run build' to build the project initially +- Watches the dist directory for asset changes +- Automatically patches screen assets when new builds are created +- Supports both single screen development and all screens + +The project directory must contain package.json with a build script. +You need to run your own build process (e.g., npm run build, npm run screen ) +to generate new assets that will be automatically detected and patched.`, + Example: ` auth0 acul dev + auth0 acul dev --dir ./my_acul_project + auth0 acul dev --screen login-id --port 3000 + auth0 acul dev -d ./project -s login-id -p 8080`, RunE: func(cmd *cobra.Command, args []string) error { - return watchAndPatch(context.Background(), cli, assetsURL, watchFolders, screens) + return runAculDev(cmd.Context(), cli, projectDir, port, screenDirs) }, } - screensFlag1.RegisterStringSlice(cmd, &screens, nil) - watchFolder.RegisterString(cmd, &watchFolders, "") - assetURL.RegisterString(cmd, &assetsURL, "") + projectDirFlag.RegisterString(cmd, &projectDir, "") + screenDevFlag.RegisterStringSlice(cmd, &screenDirs, nil) + portFlag.RegisterString(cmd, &port, "8080") return cmd } +func runAculDev(ctx context.Context, cli *cli, projectDir, port string, screenDirs []string) error { + // Default to current directory + if projectDir == "" { + projectDir = "." + } + + // Validate project structure + if err := validateAculProject(projectDir); err != nil { + return fmt.Errorf("invalid ACUL project: %w", err) + } + + log.Printf("🚀 Starting ACUL development mode for project in %s", projectDir) + + // Initial build + log.Println("🔨 Running initial build...") + if err := buildProject(projectDir); err != nil { + return fmt.Errorf("initial build failed: %w", err) + } + + // Start asset watching and patching using existing logic + log.Println("👀 Starting asset watcher...") + log.Println("💡 Run 'npm run build' or 'npm run screen ' to generate new assets that will be automatically patched") + + //ToDO: Add log that says: Host your own server to serve the built assets in the same port.(Ex: like using 'npx serve dist -l ') + + assetsURL := fmt.Sprintf("http://localhost:%s", port) + distPath := filepath.Join(projectDir, "dist") + + log.Printf("🌐 Assets URL: %s", assetsURL) + log.Printf("👀 Watching screens: %v", screenDirs) + + // Reuse the existing watchAndPatch function + return watchAndPatch(ctx, cli, assetsURL, distPath, screenDirs) +} + +func validateAculProject(projectDir string) error { + // Check for package.json + packagePath := filepath.Join(projectDir, "package.json") + if _, err := os.Stat(packagePath); os.IsNotExist(err) { + return fmt.Errorf("package.json not found. This doesn't appear to be a valid ACUL project") + } + + // Check for src directory (typical for ACUL projects) + srcPath := filepath.Join(projectDir, "src") + if _, err := os.Stat(srcPath); os.IsNotExist(err) { + return fmt.Errorf("src directory not found. This doesn't appear to be a valid ACUL project structure") + } + + return nil +} + +func buildProject(projectDir string) error { + cmd := exec.Command("npm", "run", "build") + cmd.Dir = projectDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("build failed: %w", err) + } + + log.Println("✅ Build completed successfully") + return nil +} + func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screenDirs []string) error { watcher, err := fsnotify.NewWatcher() if err != nil { @@ -90,15 +166,11 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } else { for _, screen := range screenDirs { path := filepath.Join(distAssetsPath, screen) - info, err := os.Stat(path) + _, err = os.Stat(path) if err != nil { log.Printf("Screen directory %q not found in dist/assets: %v", screen, err) continue } - if !info.IsDir() { - log.Printf("Screen path %q exists but is not a directory", path) - continue - } screensToWatch = append(screensToWatch, screen) } } @@ -120,7 +192,8 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc return nil } - if strings.HasSuffix(event.Name, "assets") && event.Op&(fsnotify.Create) != 0 { + // React to changes in dist/assets directory + if strings.HasSuffix(event.Name, "assets") && event.Op&fsnotify.Create != 0 { now := time.Now() if now.Sub(lastProcessTime) < debounceWindow { log.Println("⏱️ Ignoring event due to debounce window") @@ -129,58 +202,63 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc lastProcessTime = now time.Sleep(500 * time.Millisecond) // short delay to let writes settle - log.Println("📦 Change detected in assets folder. Rebuilding and patching...") + log.Println("📦 Change detected in assets folder. Rebuilding and patching assets...") - var wg sync.WaitGroup - errChan := make(chan error, len(screensToWatch)) + // Patch the assets + patchAssets(ctx, cli, distPath, assetsURL, screensToWatch, lastHeadTags) + } - for _, screen := range screensToWatch { - wg.Add(1) + case err := <-watcher.Errors: + log.Printf("⚠️ Watcher error: %v", err) - go func(screen string) { - defer wg.Done() + case <-ctx.Done(): + return ctx.Err() + } + } +} - headTags, err := buildHeadTagsFromDirs(filepath.Dir(distAssetsPath), assetsURL, screen) - if err != nil { - errChan <- fmt.Errorf("failed to build headTags for %s: %w", screen, err) - return - } +func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, screensToWatch []string, lastHeadTags map[string][]interface{}) { + var wg sync.WaitGroup + errChan := make(chan error, len(screensToWatch)) - if reflect.DeepEqual(lastHeadTags[screen], headTags) { - log.Printf("🔁 Skipping patch for '%s' — headTags unchanged", screen) - return - } + for _, screen := range screensToWatch { + wg.Add(1) - log.Printf("📦 Detected changes for screen '%s'", screen) - lastHeadTags[screen] = headTags + go func(screen string) { + defer wg.Done() - var settings = &management.PromptRendering{ - HeadTags: headTags, - } + headTags, err := buildHeadTagsFromDirs(distPath, assetsURL, screen) + if err != nil { + errChan <- fmt.Errorf("failed to build headTags for %s: %w", screen, err) + return + } - if err = cli.api.Prompt.UpdateRendering(ctx, management.PromptType(ScreenPromptMap[screen]), management.ScreenName(screen), settings); err != nil { - errChan <- fmt.Errorf("failed to patch settings for %s: %w", screen, err) - return - } + if reflect.DeepEqual(lastHeadTags[screen], headTags) { + log.Printf("🔁 Skipping patch for '%s' — headTags unchanged", screen) + return + } - log.Printf("✅ Successfully patched screen '%s'", screen) - }(screen) - } + log.Printf("📦 Detected changes for screen '%s'", screen) + lastHeadTags[screen] = headTags - wg.Wait() - close(errChan) + settings := &management.PromptRendering{ + HeadTags: headTags, + } - for err = range errChan { - log.Println(err) - } + if err = cli.api.Prompt.UpdateRendering(ctx, management.PromptType(ScreenPromptMap[screen]), management.ScreenName(screen), settings); err != nil { + errChan <- fmt.Errorf("failed to patch settings for %s: %w", screen, err) + return } - case err = <-watcher.Errors: - log.Println("Watcher error: ", err) + log.Printf("✅ Successfully patched screen '%s'", screen) + }(screen) + } - case <-ctx.Done(): - return ctx.Err() - } + wg.Wait() + close(errChan) + + for err := range errChan { + log.Println("Watcher error: ", err) } } diff --git a/internal/cli/universal_login.go b/internal/cli/universal_login.go index 23a4366cd..836430260 100644 --- a/internal/cli/universal_login.go +++ b/internal/cli/universal_login.go @@ -64,7 +64,6 @@ func universalLoginCmd(cli *cli) *cobra.Command { cmd.SetUsageTemplate(resourceUsageTemplate()) cmd.AddCommand(customizeUniversalLoginCmd(cli)) - cmd.AddCommand(newUpdateAssetsCmd(cli)) cmd.AddCommand(showUniversalLoginCmd(cli)) cmd.AddCommand(updateUniversalLoginCmd(cli)) cmd.AddCommand(universalLoginTemplatesCmd(cli)) From 13229eff8fd9f3aa8d1a5cd0e2531da170e3eb10 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Fri, 31 Oct 2025 07:34:52 +0530 Subject: [PATCH 36/58] Add 'connected' flag to dev command for advanced rendering updates --- internal/cli/acul_dev_connected.go | 197 +++++++++++++++++++++++------ 1 file changed, 156 insertions(+), 41 deletions(-) diff --git a/internal/cli/acul_dev_connected.go b/internal/cli/acul_dev_connected.go index fd8dbc77c..45df00e8c 100644 --- a/internal/cli/acul_dev_connected.go +++ b/internal/cli/acul_dev_connected.go @@ -3,7 +3,6 @@ package cli import ( "context" "fmt" - "log" "os" "os/exec" "path/filepath" @@ -12,6 +11,8 @@ import ( "sync" "time" + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/prompt" "github.com/auth0/go-auth0/management" "github.com/fsnotify/fsnotify" "github.com/spf13/cobra" @@ -43,11 +44,20 @@ var ( Help: "Port for the local development server (default: 8080).", IsRequired: false, } + + connectedFlag = Flag{ + Name: "Connected", + LongForm: "connected", + ShortForm: "c", + Help: "Enable connected mode to update advance rendering settings of Auth0 tenant. Use only on stage/dev tenants.", + IsRequired: false, + } ) func aculDevCmd(cli *cli) *cobra.Command { var projectDir, port string var screenDirs []string + var connected bool cmd := &cobra.Command{ Use: "dev", @@ -59,56 +69,167 @@ func aculDevCmd(cli *cli) *cobra.Command { - Supports both single screen development and all screens The project directory must contain package.json with a build script. -You need to run your own build process (e.g., npm run build, npm run screen ) -to generate new assets that will be automatically detected and patched.`, + +In normal mode, you need to run your own build process (e.g., npm run build, npm run screen ) +to generate new assets that will be automatically detected and patched. + +In connected mode (--connected), this command will: +- Update the advance rendering settings of the chosen screens in your Auth0 tenant +- Run initial build and ask you to host assets locally +- Optionally run build:watch in the background for continuous asset updates +- Watch and patch assets automatically when changes are detected + +⚠️ Connected mode should only be used on stage/dev tenants, not production!`, Example: ` auth0 acul dev auth0 acul dev --dir ./my_acul_project auth0 acul dev --screen login-id --port 3000 - auth0 acul dev -d ./project -s login-id -p 8080`, + auth0 acul dev -d ./project -s login-id -p 8080 + auth0 acul dev --connected --screen login-id --port 8080`, RunE: func(cmd *cobra.Command, args []string) error { - return runAculDev(cmd.Context(), cli, projectDir, port, screenDirs) + return runAculDev(cmd.Context(), cli, projectDir, port, screenDirs, connected) }, } - projectDirFlag.RegisterString(cmd, &projectDir, "") + projectDirFlag.RegisterString(cmd, &projectDir, ".") screenDevFlag.RegisterStringSlice(cmd, &screenDirs, nil) portFlag.RegisterString(cmd, &port, "8080") + connectedFlag.RegisterBool(cmd, &connected, false) return cmd } -func runAculDev(ctx context.Context, cli *cli, projectDir, port string, screenDirs []string) error { - // Default to current directory - if projectDir == "" { - projectDir = "." - } - +func runAculDev(ctx context.Context, cli *cli, projectDir, port string, screenDirs []string, connected bool) error { // Validate project structure if err := validateAculProject(projectDir); err != nil { return fmt.Errorf("invalid ACUL project: %w", err) } - log.Printf("🚀 Starting ACUL development mode for project in %s", projectDir) + if connected { + return runConnectedMode(ctx, cli, projectDir, port, screenDirs) + } + + return runNormalMode(projectDir, port, screenDirs) +} + +func runNormalMode(projectDir, port string, screenDirs []string) error { + fmt.Printf("🚀 Starting ACUL development mode for project in %s\n", projectDir) + fmt.Printf("📋 Development server will typically be available at: %s\n\n", fmt.Sprintf("http://localhost:%s", port)) + fmt.Println("💡 Make changes to your code and view the live changes as we have HMR enabled!") + + // Run npm run dev command + //cmd := exec.Command("npm", "run", "dev", "--", "--port", port) + //ToDo: change back to use cmd once run dev command gets supported + cmd := exec.Command("npm", "run", "screen", screenDirs[0]) + cmd.Dir = projectDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // ToDo: update when changed to dev command + fmt.Printf("🔄 Executing: %s\n", ansi.Cyan("npm run screen ")) + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to run 'npm run dev': %w", err) + } + + return nil +} + +func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, screenDirs []string) error { + // Show warning and ask for confirmation with highlighted text + cli.renderer.Warnf("") + cli.renderer.Warnf("⚠️ %s", ansi.Bold("🌟 CONNECTED MODE ENABLED 🌟")) + cli.renderer.Warnf("") + cli.renderer.Infof("📢 %s", ansi.Cyan("This connected mode updates the advanced rendering settings")) + cli.renderer.Infof(" %s", ansi.Cyan("of the chosen set of screens in your Auth0 tenant.")) + cli.renderer.Warnf("") + cli.renderer.Errorf("🚨 %s", ansi.Bold("IMPORTANT: Use this ONLY on stage and dev tenants, NOT on production!")) + //ToDO: Highlight the reason: If used on prod tenants which leads to upgrade configs with the updated localHost served ASSETS URL and may lead the user to incur unexpected charges for the users on production tenants. + + cli.renderer.Warnf("") + + // // Give user time to read the warning + // cli.renderer.Infof("📖 Please take a moment to read the above warning carefully...") + // cli.renderer.Infof(" Press Enter to continue...") + // fmt.Scanln() // Wait for user to press Enter + + // Ask for confirmation + if confirmed := prompt.Confirm("Do you want to proceed with connected mode?"); !confirmed { + cli.renderer.Warnf("❌ Connected mode cancelled.") + return nil + } + + cli.renderer.Infof("") + cli.renderer.Infof("🚀 Starting ACUL connected development mode for project in %s", ansi.Green(projectDir)) - // Initial build - log.Println("🔨 Running initial build...") - if err := buildProject(projectDir); err != nil { + // Step 1: Do initial build + cli.renderer.Infof("") + cli.renderer.Infof("🔨 %s", ansi.Bold("Step 1: Running initial build...")) + if err := buildProject(cli, projectDir); err != nil { return fmt.Errorf("initial build failed: %w", err) } - // Start asset watching and patching using existing logic - log.Println("👀 Starting asset watcher...") - log.Println("💡 Run 'npm run build' or 'npm run screen ' to generate new assets that will be automatically patched") + // Step 2: Ask user to host assets and get port confirmation + cli.renderer.Infof("") + cli.renderer.Infof("📡 %s", ansi.Bold("Step 2: Host your assets locally")) + cli.renderer.Infof("Please either run the following command in a separate terminal to serve your assets or host someway on your own") + cli.renderer.Infof(" %s", ansi.Cyan(fmt.Sprintf("npx serve dist -p %s --cors", port))) + cli.renderer.Infof("") + cli.renderer.Infof("This will serve your built assets at the specified port with CORS enabled.") + + assetsHosted := prompt.Confirm(fmt.Sprintf("Are you hosting the assets at http://localhost:%s?", port)) + if !assetsHosted { + cli.renderer.Warnf("❌ Please host your assets first and run the command again.") + return nil + } + + // Step 3: Ask about build:watch + cli.renderer.Infof("") + cli.renderer.Infof("🔧 %s", ansi.Bold("Step 3: Continuous build watching (optional)")) + cli.renderer.Infof("To ensure assets are updated with sample app code changes, you can:") + cli.renderer.Infof("1. Manually re-run %s when you make changes, OR", ansi.Cyan("'npm run build'")) + cli.renderer.Infof("2. Run %s in the background for continuous updates", ansi.Cyan("'npm run build:watch'")) + cli.renderer.Infof("") + cli.renderer.Infof("💡 Note: If you have auto-save enabled in your IDE, build:watch will rebuild") + cli.renderer.Infof(" assets frequently (potentially every 15 seconds with changes).") + + runBuildWatch := prompt.Confirm("Would you like to run 'npm run build:watch' in the background?") + + var buildWatchCmd *exec.Cmd + if runBuildWatch { + cli.renderer.Infof("🔄 Starting %s in the background...", ansi.Cyan("'npm run build:watch'")) + buildWatchCmd = exec.Command("npm", "run", "build:watch") + buildWatchCmd.Dir = projectDir + buildWatchCmd.Stdout = os.Stdout + buildWatchCmd.Stderr = os.Stderr + + if err := buildWatchCmd.Start(); err != nil { + cli.renderer.Warnf("⚠️ Failed to start build:watch: %v", err) + cli.renderer.Infof("You can manually run %s when you make changes.", ansi.Cyan("'npm run build'")) + } else { + cli.renderer.Infof("✅ Build watch started successfully") + // Ensure the process is killed when the main process exits + defer func() { + if buildWatchCmd.Process != nil { + buildWatchCmd.Process.Kill() + } + }() + } + } - //ToDO: Add log that says: Host your own server to serve the built assets in the same port.(Ex: like using 'npx serve dist -l ') + // Step 4: Start watching and patching + cli.renderer.Infof("") + cli.renderer.Infof("👀 %s", ansi.Bold("Step 4: Starting asset watcher and patching...")) assetsURL := fmt.Sprintf("http://localhost:%s", port) distPath := filepath.Join(projectDir, "dist") - log.Printf("🌐 Assets URL: %s", assetsURL) - log.Printf("👀 Watching screens: %v", screenDirs) + cli.renderer.Infof("🌐 Assets URL: %s", ansi.Green(assetsURL)) + cli.renderer.Infof("👀 Watching screens: %v", screenDirs) + cli.renderer.Infof("💡 Assets will be automatically patched when changes are detected in the dist folder") + + //ToDO: Give the user a hint to trigger the `auth0 test login` command to see the changes in action in their tenant's application. - // Reuse the existing watchAndPatch function + // Start watching and patching return watchAndPatch(ctx, cli, assetsURL, distPath, screenDirs) } @@ -119,16 +240,10 @@ func validateAculProject(projectDir string) error { return fmt.Errorf("package.json not found. This doesn't appear to be a valid ACUL project") } - // Check for src directory (typical for ACUL projects) - srcPath := filepath.Join(projectDir, "src") - if _, err := os.Stat(srcPath); os.IsNotExist(err) { - return fmt.Errorf("src directory not found. This doesn't appear to be a valid ACUL project structure") - } - return nil } -func buildProject(projectDir string) error { +func buildProject(cli *cli, projectDir string) error { cmd := exec.Command("npm", "run", "build") cmd.Dir = projectDir cmd.Stdout = os.Stdout @@ -138,7 +253,7 @@ func buildProject(projectDir string) error { return fmt.Errorf("build failed: %w", err) } - log.Println("✅ Build completed successfully") + cli.renderer.Infof("✅ Build completed successfully") return nil } @@ -168,7 +283,7 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc path := filepath.Join(distAssetsPath, screen) _, err = os.Stat(path) if err != nil { - log.Printf("Screen directory %q not found in dist/assets: %v", screen, err) + cli.renderer.Warnf("Screen directory %q not found in dist/assets: %v", screen, err) continue } screensToWatch = append(screensToWatch, screen) @@ -176,9 +291,9 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } if err := watcher.Add(distPath); err != nil { - log.Printf("Failed to watch %q: %v", distPath, err) + cli.renderer.Warnf("Failed to watch %q: %v", distPath, err) } else { - log.Printf("👀 Watching: %d screen(s): %v", len(screensToWatch), screensToWatch) + cli.renderer.Infof("👀 Watching: %d screen(s): %v", len(screensToWatch), screensToWatch) } const debounceWindow = 5 * time.Second @@ -196,20 +311,20 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc if strings.HasSuffix(event.Name, "assets") && event.Op&fsnotify.Create != 0 { now := time.Now() if now.Sub(lastProcessTime) < debounceWindow { - log.Println("⏱️ Ignoring event due to debounce window") + cli.renderer.Infof("⏱️ Ignoring event due to debounce window") continue } lastProcessTime = now time.Sleep(500 * time.Millisecond) // short delay to let writes settle - log.Println("📦 Change detected in assets folder. Rebuilding and patching assets...") + cli.renderer.Infof("📦 Change detected in assets folder. Rebuilding and patching assets...") // Patch the assets patchAssets(ctx, cli, distPath, assetsURL, screensToWatch, lastHeadTags) } case err := <-watcher.Errors: - log.Printf("⚠️ Watcher error: %v", err) + cli.renderer.Warnf("⚠️ Watcher error: %v", err) case <-ctx.Done(): return ctx.Err() @@ -234,11 +349,11 @@ func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, scre } if reflect.DeepEqual(lastHeadTags[screen], headTags) { - log.Printf("🔁 Skipping patch for '%s' — headTags unchanged", screen) + cli.renderer.Infof("🔁 Skipping patch for '%s' — headTags unchanged", screen) return } - log.Printf("📦 Detected changes for screen '%s'", screen) + cli.renderer.Infof("📦 Detected changes for screen '%s'", ansi.Cyan(screen)) lastHeadTags[screen] = headTags settings := &management.PromptRendering{ @@ -250,7 +365,7 @@ func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, scre return } - log.Printf("✅ Successfully patched screen '%s'", screen) + cli.renderer.Infof("✅ Successfully patched screen '%s'", ansi.Green(screen)) }(screen) } @@ -258,7 +373,7 @@ func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, scre close(errChan) for err := range errChan { - log.Println("Watcher error: ", err) + cli.renderer.Errorf("⚠️ Watcher error: %v", err) } } From 5e56153dbaf97dc8850185eab6bac8b8f9454d44 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 3 Nov 2025 10:45:22 +0530 Subject: [PATCH 37/58] Refactor acul dev command and improve documentation for dev and connected modes --- internal/cli/acul_dev_connected.go | 326 ++++++++++++++++++----------- 1 file changed, 203 insertions(+), 123 deletions(-) diff --git a/internal/cli/acul_dev_connected.go b/internal/cli/acul_dev_connected.go index 45df00e8c..e81d76ed7 100644 --- a/internal/cli/acul_dev_connected.go +++ b/internal/cli/acul_dev_connected.go @@ -19,7 +19,6 @@ import ( ) var ( - // New flags for acul dev command projectDirFlag = Flag{ Name: "Project Directory", LongForm: "dir", @@ -27,24 +26,21 @@ var ( Help: "Path to the ACUL project directory (must contain package.json).", IsRequired: false, } - screenDevFlag = Flag{ - Name: "Screen", + Name: "Screens", LongForm: "screen", ShortForm: "s", - Help: "Specific screen to develop and watch. If not provided, will watch all screens in the dist/assets folder.", + Help: "Specific screens to develop and watch. Required for both dev and connected modes. Can specify multiple screens.", IsRequired: false, AlwaysPrompt: false, } - portFlag = Flag{ Name: "Port", LongForm: "port", ShortForm: "p", - Help: "Port for the local development server (default: 8080).", + Help: "Port for the local development server. Used in dev mode only (default: 8080).", IsRequired: false, } - connectedFlag = Flag{ Name: "Connected", LongForm: "connected", @@ -70,21 +66,28 @@ func aculDevCmd(cli *cli) *cobra.Command { The project directory must contain package.json with a build script. -In normal mode, you need to run your own build process (e.g., npm run build, npm run screen ) -to generate new assets that will be automatically detected and patched. +DEV MODE (default): +- Requires: --screen flag to specify which screens to develop +- Requires: --port flag for the local development server +- Runs your build process (e.g., npm run screen ) for HMR development -In connected mode (--connected), this command will: -- Update the advance rendering settings of the chosen screens in your Auth0 tenant -- Run initial build and ask you to host assets locally -- Optionally run build:watch in the background for continuous asset updates -- Watch and patch assets automatically when changes are detected +CONNECTED MODE (--connected): +- Requires: --screen flag to specify screens to patch in Auth0 tenant +- Updates advance rendering settings of the chosen screens in your Auth0 tenant +- Runs initial build and expects you to host assets locally +- Optionally runs build:watch in the background for continuous asset updates +- Watches and patches assets automatically when changes are detected ⚠️ Connected mode should only be used on stage/dev tenants, not production!`, - Example: ` auth0 acul dev - auth0 acul dev --dir ./my_acul_project - auth0 acul dev --screen login-id --port 3000 - auth0 acul dev -d ./project -s login-id -p 8080 - auth0 acul dev --connected --screen login-id --port 8080`, + Example: ` # Dev mode + auth0 acul dev --port 3000 + auth0 acul dev --port 8080 + auth0 acul dev -p 8080 --dir ./my_project + + # Connected mode (requires --screen) + auth0 acul dev --connected --screen login-id + auth0 acul dev --connected --screen login-id,signup + auth0 acul dev -c -s login-id -s signup`, RunE: func(cmd *cobra.Command, args []string) error { return runAculDev(cmd.Context(), cli, projectDir, port, screenDirs, connected) }, @@ -92,122 +95,202 @@ In connected mode (--connected), this command will: projectDirFlag.RegisterString(cmd, &projectDir, ".") screenDevFlag.RegisterStringSlice(cmd, &screenDirs, nil) - portFlag.RegisterString(cmd, &port, "8080") + portFlag.RegisterString(cmd, &port, "") connectedFlag.RegisterBool(cmd, &connected, false) return cmd } func runAculDev(ctx context.Context, cli *cli, projectDir, port string, screenDirs []string, connected bool) error { - // Validate project structure + // TODO: If projDir is empty ensure to speak about defaulting to the current folder which is `.` if they don't want to use the curr Director with the use of dir flag + if err := validateAculProject(projectDir); err != nil { return fmt.Errorf("invalid ACUL project: %w", err) } if connected { + if len(screenDirs) == 0 { + // ToDO: Prompt to chose from the screens existing in projDir/src/screens by saying the available screens in the proj folder with the use of screens FLag + } + return runConnectedMode(ctx, cli, projectDir, port, screenDirs) + } else { + // Normal dev mode validation + if len(screenDirs) == 0 { + return fmt.Errorf("dev mode requires a screen to be specified with --screen flag") + } + if port == "" { + return fmt.Errorf("dev mode requires a port to be specified with --port flag") + } + return runNormalMode(cli, projectDir, port, screenDirs) } - - return runNormalMode(projectDir, port, screenDirs) } -func runNormalMode(projectDir, port string, screenDirs []string) error { - fmt.Printf("🚀 Starting ACUL development mode for project in %s\n", projectDir) - fmt.Printf("📋 Development server will typically be available at: %s\n\n", fmt.Sprintf("http://localhost:%s", port)) - fmt.Println("💡 Make changes to your code and view the live changes as we have HMR enabled!") +func runNormalMode(cli *cli, projectDir, port string, screenDirs []string) error { + fmt.Println(ansi.Bold("🚀 Starting ") + ansi.Cyan("ACUL Dev Mode")) + + fmt.Printf("📂 Project: %s\n", ansi.Yellow(projectDir)) + + // Temporarily fixed port for dev mode + port = "3000" //ToDo : fix the port logic; + fmt.Printf("🖥️ Server: %s\n", ansi.Green(fmt.Sprintf("http://localhost:%s", port))) + fmt.Println("💡 " + ansi.Italic("Edit your code and see live changes instantly (HMR enabled)")) + + screen := screenDirs[0] - // Run npm run dev command - //cmd := exec.Command("npm", "run", "dev", "--", "--port", port) //ToDo: change back to use cmd once run dev command gets supported - cmd := exec.Command("npm", "run", "screen", screenDirs[0]) + // Run npm run dev command + cmd := exec.Command("npm", "run", "screen", screen) cmd.Dir = projectDir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - // ToDo: update when changed to dev command - fmt.Printf("🔄 Executing: %s\n", ansi.Cyan("npm run screen ")) + // Show output only in debug mode + if cli.debug { + fmt.Println("\n🔄 Running:", ansi.Cyan(fmt.Sprintf("npm run screen %s", screen))) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to run 'npm run dev': %w", err) + return fmt.Errorf("❌ failed to run 'npm run screen %s': %w", screen, err) } return nil } func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, screenDirs []string) error { - // Show warning and ask for confirmation with highlighted text - cli.renderer.Warnf("") - cli.renderer.Warnf("⚠️ %s", ansi.Bold("🌟 CONNECTED MODE ENABLED 🌟")) - cli.renderer.Warnf("") - cli.renderer.Infof("📢 %s", ansi.Cyan("This connected mode updates the advanced rendering settings")) - cli.renderer.Infof(" %s", ansi.Cyan("of the chosen set of screens in your Auth0 tenant.")) - cli.renderer.Warnf("") - cli.renderer.Errorf("🚨 %s", ansi.Bold("IMPORTANT: Use this ONLY on stage and dev tenants, NOT on production!")) - //ToDO: Highlight the reason: If used on prod tenants which leads to upgrade configs with the updated localHost served ASSETS URL and may lead the user to incur unexpected charges for the users on production tenants. - - cli.renderer.Warnf("") - - // // Give user time to read the warning - // cli.renderer.Infof("📖 Please take a moment to read the above warning carefully...") - // cli.renderer.Infof(" Press Enter to continue...") - // fmt.Scanln() // Wait for user to press Enter - - // Ask for confirmation - if confirmed := prompt.Confirm("Do you want to proceed with connected mode?"); !confirmed { - cli.renderer.Warnf("❌ Connected mode cancelled.") + fmt.Println("") + fmt.Println("📢 " + ansi.Bold(ansi.Cyan("Connected Mode Information"))) + fmt.Println("") + fmt.Println("� " + ansi.Cyan("This mode updates advanced rendering settings for selected screens in your Auth0 tenant.")) + fmt.Println("🚨 " + ansi.Bold(ansi.Red("IMPORTANT: Never use on production tenants!"))) + fmt.Println(" " + ansi.Yellow("Production may break sessions or incur unexpected charges with local assets.")) + fmt.Println(" " + ansi.Yellow("Use ONLY for dev/stage tenants.")) + + fmt.Println("") + fmt.Println("⚙️ " + ansi.Bold(ansi.Magenta("Technical Requirements:"))) + fmt.Println(" " + ansi.Cyan("• Requires sample apps with viteConfig.ts configured for asset building")) + fmt.Println(" " + ansi.Cyan("• Assets must be built in the following structure:")) + fmt.Println(" " + ansi.Green("assets//")) + fmt.Println(" " + ansi.Green("assets//")) + fmt.Println(" " + ansi.Green("assets/")) + fmt.Println("") + fmt.Println("🔄 " + ansi.Bold(ansi.Magenta("How it works:"))) + fmt.Println(" " + ansi.Cyan("• Combines files from screen-specific, shared, and main asset folders")) + fmt.Println(" " + ansi.Cyan("• Makes API patch calls to update rendering settings for each specified screen")) + fmt.Println(" " + ansi.Cyan("• Watches for changes and automatically re-patches when assets are rebuilt")) + fmt.Println("") + + if confirmed := prompt.Confirm("Proceed with connected mode?"); !confirmed { + fmt.Println(ansi.Red("❌ Connected mode cancelled.")) return nil } - cli.renderer.Infof("") - cli.renderer.Infof("🚀 Starting ACUL connected development mode for project in %s", ansi.Green(projectDir)) + // Show confirmation banner after user agrees + fmt.Println("") + fmt.Println("⚠️ " + ansi.Bold(ansi.Yellow("🌟 CONNECTED MODE ENABLED 🌟"))) + fmt.Println("") + fmt.Println("🚀 " + ansi.Green(fmt.Sprintf("ACUL connected dev mode started for %s", projectDir))) - // Step 1: Do initial build - cli.renderer.Infof("") - cli.renderer.Infof("🔨 %s", ansi.Bold("Step 1: Running initial build...")) + fmt.Println("") + fmt.Println("🔨 " + ansi.Bold(ansi.Blue("Step 1: Running initial build..."))) if err := buildProject(cli, projectDir); err != nil { return fmt.Errorf("initial build failed: %w", err) } - // Step 2: Ask user to host assets and get port confirmation - cli.renderer.Infof("") - cli.renderer.Infof("📡 %s", ansi.Bold("Step 2: Host your assets locally")) - cli.renderer.Infof("Please either run the following command in a separate terminal to serve your assets or host someway on your own") - cli.renderer.Infof(" %s", ansi.Cyan(fmt.Sprintf("npx serve dist -p %s --cors", port))) - cli.renderer.Infof("") - cli.renderer.Infof("This will serve your built assets at the specified port with CORS enabled.") - - assetsHosted := prompt.Confirm(fmt.Sprintf("Are you hosting the assets at http://localhost:%s?", port)) - if !assetsHosted { - cli.renderer.Warnf("❌ Please host your assets first and run the command again.") - return nil + fmt.Println("") + fmt.Println("📡 " + ansi.Bold(ansi.Blue("Step 2: Host your assets locally"))) + + if port == "" { + var portInput string + portQuestion := prompt.TextInput( + "port", + "Enter the port for serving assets:", + "The port number where your assets will be hosted (e.g., 8080)", + "8080", + true, + ) + if err := prompt.AskOne(portQuestion, &portInput); err != nil { + return fmt.Errorf("failed to get port: %w", err) + } + port = portInput } - // Step 3: Ask about build:watch - cli.renderer.Infof("") - cli.renderer.Infof("🔧 %s", ansi.Bold("Step 3: Continuous build watching (optional)")) - cli.renderer.Infof("To ensure assets are updated with sample app code changes, you can:") - cli.renderer.Infof("1. Manually re-run %s when you make changes, OR", ansi.Cyan("'npm run build'")) - cli.renderer.Infof("2. Run %s in the background for continuous updates", ansi.Cyan("'npm run build:watch'")) - cli.renderer.Infof("") - cli.renderer.Infof("💡 Note: If you have auto-save enabled in your IDE, build:watch will rebuild") - cli.renderer.Infof(" assets frequently (potentially every 15 seconds with changes).") + fmt.Println("💡 " + ansi.Yellow("Your assets need to be served locally with CORS enabled.")) + + runServe := prompt.Confirm(fmt.Sprintf("Would you like to host the assets by running 'npx serve dist -p %s --cors' in the background?", port)) + + var ( + serveCmd *exec.Cmd + serveStarted bool + ) + if runServe { + fmt.Println("🚀 " + ansi.Cyan("Starting local server in the background...")) + + serveCmd = exec.Command("npx", "serve", "dist", "-p", port, "--cors") + serveCmd.Dir = projectDir + + if cli.debug { + serveCmd.Stdout = os.Stdout + serveCmd.Stderr = os.Stderr + } + + if err := serveCmd.Start(); err != nil { + fmt.Println("⚠️ " + ansi.Yellow("Failed to start local server: ") + ansi.Bold(err.Error())) + fmt.Println(" You can manually run " + ansi.Cyan(fmt.Sprintf("'npx serve dist -p %s --cors'", port)) + " in a separate terminal.") + } else { + serveStarted = true + fmt.Println("✅ " + ansi.Green("Local server started successfully at ") + + ansi.Cyan(fmt.Sprintf("http://localhost:%s", port))) + defer func() { + if serveCmd.Process != nil { + serveCmd.Process.Kill() + } + }() + } + } else { + fmt.Println("📋 " + ansi.Cyan("Please host your assets manually using:")) + fmt.Println(" " + ansi.Bold(ansi.Green(fmt.Sprintf("npx serve dist -p %s --cors", port)))) + fmt.Println("") + fmt.Println("💡 " + ansi.Yellow("This will serve your built assets with CORS enabled.")) + } + + assetsURL := fmt.Sprintf("http://localhost:%s", port) + + // Only ask confirmation if not started in background + if !serveStarted { + assetsHosted := prompt.Confirm(fmt.Sprintf("Are your assets hosted and accessible at %s?", assetsURL)) + if !assetsHosted { + cli.renderer.Warnf("❌ Please host your assets first and run the command again.") + return nil + } + } + + fmt.Println("") + fmt.Println("🔧 " + ansi.Bold(ansi.Blue("Step 3: Continuous build watching (optional)"))) + fmt.Println(" " + ansi.Green("1. Manually run 'npm run build' after changes, OR")) + fmt.Println(" " + ansi.Green("2. Run 'npm run build:watch' for continuous updates")) + fmt.Println("") + fmt.Println("💡 " + ansi.Yellow("Note: If auto-save is enabled in your IDE, build:watch will rebuild frequently.")) runBuildWatch := prompt.Confirm("Would you like to run 'npm run build:watch' in the background?") var buildWatchCmd *exec.Cmd if runBuildWatch { - cli.renderer.Infof("🔄 Starting %s in the background...", ansi.Cyan("'npm run build:watch'")) + fmt.Println("🔄 " + ansi.Cyan("Starting 'npm run build:watch' in the background...")) buildWatchCmd = exec.Command("npm", "run", "build:watch") buildWatchCmd.Dir = projectDir - buildWatchCmd.Stdout = os.Stdout - buildWatchCmd.Stderr = os.Stderr + + // Only show command output if debug mode is enabled. + if cli.debug { + buildWatchCmd.Stdout = os.Stdout + buildWatchCmd.Stderr = os.Stderr + } if err := buildWatchCmd.Start(); err != nil { - cli.renderer.Warnf("⚠️ Failed to start build:watch: %v", err) - cli.renderer.Infof("You can manually run %s when you make changes.", ansi.Cyan("'npm run build'")) + fmt.Println("⚠️ " + ansi.Yellow("Failed to start build:watch: ") + ansi.Bold(err.Error())) + fmt.Println(" You can manually run " + ansi.Cyan("'npm run build'") + " when changes are made.") } else { - cli.renderer.Infof("✅ Build watch started successfully") - // Ensure the process is killed when the main process exits + fmt.Println("✅ " + ansi.Green("Build watch started successfully")) defer func() { if buildWatchCmd.Process != nil { buildWatchCmd.Process.Kill() @@ -216,44 +299,43 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc } } - // Step 4: Start watching and patching - cli.renderer.Infof("") - cli.renderer.Infof("👀 %s", ansi.Bold("Step 4: Starting asset watcher and patching...")) + fmt.Println("") + fmt.Println("👀 " + ansi.Bold(ansi.Blue("Step 4: Starting asset watcher and patching..."))) - assetsURL := fmt.Sprintf("http://localhost:%s", port) distPath := filepath.Join(projectDir, "dist") - cli.renderer.Infof("🌐 Assets URL: %s", ansi.Green(assetsURL)) - cli.renderer.Infof("👀 Watching screens: %v", screenDirs) - cli.renderer.Infof("💡 Assets will be automatically patched when changes are detected in the dist folder") + fmt.Println("🌐 Assets URL: " + ansi.Green(assetsURL)) + fmt.Println("👀 Watching screens: " + ansi.Cyan(strings.Join(screenDirs, ", "))) + fmt.Println("💡 " + ansi.Green("Assets will be patched automatically when changes are detected in the dist folder")) + fmt.Println("") + fmt.Println("🧪 " + ansi.Bold(ansi.Magenta("Tip: Run 'auth0 test login' to see your changes in action!"))) - //ToDO: Give the user a hint to trigger the `auth0 test login` command to see the changes in action in their tenant's application. - - // Start watching and patching return watchAndPatch(ctx, cli, assetsURL, distPath, screenDirs) } func validateAculProject(projectDir string) error { - // Check for package.json packagePath := filepath.Join(projectDir, "package.json") if _, err := os.Stat(packagePath); os.IsNotExist(err) { return fmt.Errorf("package.json not found. This doesn't appear to be a valid ACUL project") } - return nil } func buildProject(cli *cli, projectDir string) error { cmd := exec.Command("npm", "run", "build") cmd.Dir = projectDir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + + // Only show command output if debug mode is enabled + if cli.debug { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } if err := cmd.Run(); err != nil { return fmt.Errorf("build failed: %w", err) } - cli.renderer.Infof("✅ Build completed successfully") + fmt.Println("✅ " + ansi.Green("Build completed successfully")) return nil } @@ -283,7 +365,7 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc path := filepath.Join(distAssetsPath, screen) _, err = os.Stat(path) if err != nil { - cli.renderer.Warnf("Screen directory %q not found in dist/assets: %v", screen, err) + fmt.Println("⚠️ " + ansi.Yellow(fmt.Sprintf("Screen directory '%s' not found in dist/assets: %v", screen, err))) continue } screensToWatch = append(screensToWatch, screen) @@ -291,9 +373,9 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } if err := watcher.Add(distPath); err != nil { - cli.renderer.Warnf("Failed to watch %q: %v", distPath, err) + fmt.Println("⚠️ " + ansi.Yellow("Failed to watch ") + ansi.Bold(distPath) + ": " + err.Error()) } else { - cli.renderer.Infof("👀 Watching: %d screen(s): %v", len(screensToWatch), screensToWatch) + fmt.Println("👀 Watching: " + ansi.Cyan(fmt.Sprintf("%d screen(s): %v", len(screensToWatch), screensToWatch))) } const debounceWindow = 5 * time.Second @@ -311,20 +393,22 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc if strings.HasSuffix(event.Name, "assets") && event.Op&fsnotify.Create != 0 { now := time.Now() if now.Sub(lastProcessTime) < debounceWindow { - cli.renderer.Infof("⏱️ Ignoring event due to debounce window") + // Only show debounce message in debug mode + if cli.debug { + cli.renderer.Infof("⏱️ %s", ansi.Yellow("Ignoring event due to debounce window")) + } continue } lastProcessTime = now - time.Sleep(500 * time.Millisecond) // short delay to let writes settle - cli.renderer.Infof("📦 Change detected in assets folder. Rebuilding and patching assets...") + time.Sleep(500 * time.Millisecond) // let writes settle + fmt.Println("📦 " + ansi.Cyan("Change detected in assets. Rebuilding and patching...")) - // Patch the assets patchAssets(ctx, cli, distPath, assetsURL, screensToWatch, lastHeadTags) } case err := <-watcher.Errors: - cli.renderer.Warnf("⚠️ Watcher error: %v", err) + fmt.Println("⚠️ " + ansi.Yellow("Watcher error: ") + ansi.Bold(err.Error())) case <-ctx.Done(): return ctx.Err() @@ -338,7 +422,6 @@ func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, scre for _, screen := range screensToWatch { wg.Add(1) - go func(screen string) { defer wg.Done() @@ -349,15 +432,16 @@ func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, scre } if reflect.DeepEqual(lastHeadTags[screen], headTags) { - cli.renderer.Infof("🔁 Skipping patch for '%s' — headTags unchanged", screen) + fmt.Println("🔁 " + ansi.Cyan(fmt.Sprintf("Skipping patch for '%s' — headTags unchanged", screen))) return } - cli.renderer.Infof("📦 Detected changes for screen '%s'", ansi.Cyan(screen)) + fmt.Println("📦 " + ansi.Cyan(fmt.Sprintf("Detected changes for '%s'", screen))) lastHeadTags[screen] = headTags settings := &management.PromptRendering{ - HeadTags: headTags, + RenderingMode: &management.RenderingModeAdvanced, + HeadTags: headTags, } if err = cli.api.Prompt.UpdateRendering(ctx, management.PromptType(ScreenPromptMap[screen]), management.ScreenName(screen), settings); err != nil { @@ -365,7 +449,7 @@ func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, scre return } - cli.renderer.Infof("✅ Successfully patched screen '%s'", ansi.Green(screen)) + fmt.Println("✅ " + ansi.Green(fmt.Sprintf("Successfully patched screen '%s'", screen))) }(screen) } @@ -373,7 +457,7 @@ func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, scre close(errChan) for err := range errChan { - cli.renderer.Errorf("⚠️ Watcher error: %v", err) + fmt.Println("⚠️ " + ansi.Yellow("Patch error: ") + ansi.Bold(err.Error())) } } @@ -382,15 +466,13 @@ func buildHeadTagsFromDirs(distPath, assetsURL, screen string) ([]interface{}, e screenPath := filepath.Join(distPath, "assets", screen) sharedPath := filepath.Join(distPath, "assets", "shared") mainPath := filepath.Join(distPath, "assets") - sources := []string{sharedPath, screenPath, mainPath} for _, dir := range sources { entries, err := os.ReadDir(dir) if err != nil { - continue // skip on error + continue } - for _, entry := range entries { if entry.IsDir() { continue @@ -398,13 +480,12 @@ func buildHeadTagsFromDirs(distPath, assetsURL, screen string) ([]interface{}, e name := entry.Name() subDir := filepath.Base(dir) if subDir == "assets" { - subDir = "" // root-level main-*.js + subDir = "" } src := fmt.Sprintf("%s/assets/%s%s", assetsURL, subDir, name) if subDir != "" { src = fmt.Sprintf("%s/assets/%s/%s", assetsURL, subDir, name) } - ext := filepath.Ext(name) switch ext { @@ -428,6 +509,5 @@ func buildHeadTagsFromDirs(distPath, assetsURL, screen string) ([]interface{}, e } } } - return tags, nil } From 2a5813142f54bb870d36e3415bf75a4e2d833605 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 3 Nov 2025 23:06:24 +0530 Subject: [PATCH 38/58] enhance ACUL scaffolding to dynamically fetch the latest release tag --- acul_config/accept-invitation.json | 8 --- config/accept-invitation.json | 28 -------- internal/cli/acul_app_scaffolding.go | 97 ++++++++++++++++++++++++++-- 3 files changed, 91 insertions(+), 42 deletions(-) delete mode 100644 acul_config/accept-invitation.json delete mode 100644 config/accept-invitation.json diff --git a/acul_config/accept-invitation.json b/acul_config/accept-invitation.json deleted file mode 100644 index 79ad72b38..000000000 --- a/acul_config/accept-invitation.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "context_configuration": [], - "default_head_tags_disabled": false, - "filters": {}, - "head_tags": [], - "rendering_mode": "standard", - "use_page_template": false -} \ No newline at end of file diff --git a/config/accept-invitation.json b/config/accept-invitation.json deleted file mode 100644 index 0f61103d8..000000000 --- a/config/accept-invitation.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "tenant": "dev-s2xt6l5qvounptri", - "prompt": "invitation", - "screen": "accept-invitation", - "rendering_mode": "advanced", - "context_configuration": [ - "screen.texts" - ], - "default_head_tags_disabled": false, - "head_tags": [ - { - "attributes": { - "async": true, - "defer": true, - "src": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.jss" - }, - "tag": "script" - }, - { - "attributes": { - "href": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js", - "rel": "stylesheet" - }, - "tag": "link" - } - ], - "use_page_template": false -} \ No newline at end of file diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index d6259e2dd..8d2446e8e 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -52,7 +52,12 @@ type Metadata struct { // loadManifest loads manifest.json once. func loadManifest() (*Manifest, error) { - url := "https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/monorepo-sample/manifest.json" + latestTag, err := getLatestReleaseTag() + if err != nil { + return nil, fmt.Errorf("failed to get latest release tag: %w", err) + } + + url := fmt.Sprintf("https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/%s/manifest.json", latestTag) resp, err := http.Get(url) if err != nil { @@ -78,6 +83,42 @@ func loadManifest() (*Manifest, error) { return &manifest, nil } +// getLatestReleaseTag fetches the latest tag from GitHub API. +func getLatestReleaseTag() (string, error) { + url := "https://api.github.com/repos/auth0-samples/auth0-acul-samples/tags" + + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("failed to fetch tags: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch tags: received status code %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + + var tags []struct { + Name string `json:"name"` + } + + if err := json.Unmarshal(body, &tags); err != nil { + return "", fmt.Errorf("failed to parse tags response: %w", err) + } + + if len(tags) == 0 { + return "", fmt.Errorf("no tags found in repository") + } + + //return tags[0].Name, nil. + + return "monorepo-sample", nil +} + var ( templateFlag = Flag{ Name: "Template", @@ -133,6 +174,11 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string, inputs *struct { return err } + latestTag, err := getLatestReleaseTag() + if err != nil { + return fmt.Errorf("failed to get latest release tag: %w", err) + } + manifest, err := loadManifest() if err != nil { return err @@ -177,7 +223,7 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string, inputs *struct { return err } - err = writeAculConfig(destDir, chosenTemplate, selectedScreens, manifest.Metadata.Version) + err = writeAculConfig(destDir, chosenTemplate, selectedScreens, manifest.Metadata.Version, latestTag) if err != nil { fmt.Printf("Failed to write config: %v\n", err) } @@ -287,6 +333,12 @@ func getDestDir(args []string) string { } func downloadAndUnzipSampleRepo() (string, error) { + _, err := getLatestReleaseTag() + if err != nil { + return "", fmt.Errorf("failed to get latest release tag: %w", err) + } + + //repoURL := fmt.Sprintf("https://github.com/auth0-samples/auth0-acul-samples/archive/refs/tags/%s.zip", latestTag). repoURL := "https://github.com/auth0-samples/auth0-acul-samples/archive/refs/heads/monorepo-sample.zip" tempZipFile := downloadFile(repoURL) defer os.Remove(tempZipFile) // Clean up the temp zip file. @@ -303,8 +355,29 @@ func downloadAndUnzipSampleRepo() (string, error) { return tempUnzipDir, nil } +// This supports any version tag (v1.0.0, v2.0.0, etc.) without hardcoding. +func findExtractedRepoDir(tempUnzipDir string) (string, error) { + entries, err := os.ReadDir(tempUnzipDir) + if err != nil { + return "", fmt.Errorf("failed to read temp directory: %w", err) + } + + for _, entry := range entries { + if entry.IsDir() && strings.HasPrefix(entry.Name(), "auth0-acul-samples-") { + return entry.Name(), nil + } + } + + return "", fmt.Errorf("could not find extracted auth0-acul-samples directory") +} + func copyTemplateBaseDirs(cli *cli, baseDirs []string, chosenTemplate, tempUnzipDir, destDir string) error { - sourcePathPrefix := filepath.Join("auth0-acul-samples-monorepo-sample", chosenTemplate) + extractedDir, err := findExtractedRepoDir(tempUnzipDir) + if err != nil { + return fmt.Errorf("failed to find extracted directory: %w", err) + } + + sourcePathPrefix := filepath.Join(extractedDir, chosenTemplate) for _, dirPath := range baseDirs { srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, dirPath) destPath := filepath.Join(destDir, dirPath) @@ -324,7 +397,12 @@ func copyTemplateBaseDirs(cli *cli, baseDirs []string, chosenTemplate, tempUnzip } func copyProjectTemplateFiles(cli *cli, baseFiles []string, chosenTemplate, tempUnzipDir, destDir string) error { - sourcePathPrefix := filepath.Join("auth0-acul-samples-monorepo-sample", chosenTemplate) + extractedDir, err := findExtractedRepoDir(tempUnzipDir) + if err != nil { + return fmt.Errorf("failed to find extracted directory: %w", err) + } + + sourcePathPrefix := filepath.Join(extractedDir, chosenTemplate) for _, filePath := range baseFiles { srcPath := filepath.Join(tempUnzipDir, sourcePathPrefix, filePath) @@ -352,7 +430,12 @@ func copyProjectTemplateFiles(cli *cli, baseFiles []string, chosenTemplate, temp } func copyProjectScreens(cli *cli, screens []Screens, selectedScreens []string, chosenTemplate, tempUnzipDir, destDir string) error { - sourcePathPrefix := "auth0-acul-samples-monorepo-sample/" + chosenTemplate + extractedDir, err := findExtractedRepoDir(tempUnzipDir) + if err != nil { + return fmt.Errorf("failed to find extracted directory: %w", err) + } + + sourcePathPrefix := extractedDir + "/" + chosenTemplate screenInfo := createScreenMap(screens) for _, s := range selectedScreens { screen := screenInfo[s] @@ -381,12 +464,13 @@ func copyProjectScreens(cli *cli, screens []Screens, selectedScreens []string, c return nil } -func writeAculConfig(destDir, chosenTemplate string, selectedScreens []string, manifestVersion string) error { +func writeAculConfig(destDir, chosenTemplate string, selectedScreens []string, manifestVersion, appVersion string) error { config := AculConfig{ ChosenTemplate: chosenTemplate, Screens: selectedScreens, InitTimestamp: time.Now().Format(time.RFC3339), AculManifestVersion: manifestVersion, + AppVersion: appVersion, } data, err := json.MarshalIndent(config, "", " ") @@ -532,6 +616,7 @@ type AculConfig struct { ChosenTemplate string `json:"chosen_template"` Screens []string `json:"screens"` InitTimestamp string `json:"init_timestamp"` + AppVersion string `json:"app_version,omitempty"` AculManifestVersion string `json:"acul_manifest_version"` } From 4f58628e1dfc08d8819d6d0d040e3f9323170b18 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 3 Nov 2025 23:37:59 +0530 Subject: [PATCH 39/58] use 'acul-sample-app' as default project name --- docs/auth0_acul_init.md | 6 +++--- internal/cli/acul_app_scaffolding.go | 13 ++++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/auth0_acul_init.md b/docs/auth0_acul_init.md index 792f81e41..f6f2769bc 100644 --- a/docs/auth0_acul_init.md +++ b/docs/auth0_acul_init.md @@ -18,9 +18,9 @@ auth0 acul init [flags] ``` auth0 acul init - auth0 acul init my_acul_app - auth0 acul init my_acul_app --template react --screens login,signup - auth0 acul init my_acul_app -t react -s login,mfa,signup + auth0 acul init acul-sample-app + auth0 acul init acul-sample-app --template react --screens login,signup + auth0 acul init acul-sample-app -t react -s login,mfa,signup ``` diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index 8d2446e8e..6c14e6bc2 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -114,8 +114,7 @@ func getLatestReleaseTag() (string, error) { return "", fmt.Errorf("no tags found in repository") } - //return tags[0].Name, nil. - + // TODO: return tags[0].Name, nil. return "monorepo-sample", nil } @@ -152,9 +151,9 @@ func aculInitCmd(cli *cli) *cobra.Command { This command creates a new project with your choice of framework and authentication screens (login, signup, mfa, etc.). The generated project includes all necessary configuration and boilerplate code to get started with ACUL customizations.`, Example: ` auth0 acul init - auth0 acul init my_acul_app - auth0 acul init my_acul_app --template react --screens login,signup - auth0 acul init my_acul_app -t react -s login,mfa,signup`, + auth0 acul init acul-sample-app + auth0 acul init acul-sample-app --template react --screens login,signup + auth0 acul init acul-sample-app -t react -s login,mfa,signup`, RunE: func(cmd *cobra.Command, args []string) error { return runScaffold(cli, cmd, args, &inputs) }, @@ -327,7 +326,7 @@ func selectScreens(cli *cli, screens []Screens, providedScreens []string) ([]str func getDestDir(args []string) string { if len(args) < 1 { - return "my_acul_proj" + return "acul-sample-app" } return args[0] } @@ -338,7 +337,7 @@ func downloadAndUnzipSampleRepo() (string, error) { return "", fmt.Errorf("failed to get latest release tag: %w", err) } - //repoURL := fmt.Sprintf("https://github.com/auth0-samples/auth0-acul-samples/archive/refs/tags/%s.zip", latestTag). + // TODO: repoURL := fmt.Sprintf("https://github.com/auth0-samples/auth0-acul-samples/archive/refs/tags/%s.zip", latestTag). repoURL := "https://github.com/auth0-samples/auth0-acul-samples/archive/refs/heads/monorepo-sample.zip" tempZipFile := downloadFile(repoURL) defer os.Remove(tempZipFile) // Clean up the temp zip file. From 5dd3e4c0d608cd54719bb9032d532cdade9d7b85 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Tue, 4 Nov 2025 15:08:29 +0530 Subject: [PATCH 40/58] enhance ACUL scaffolding to support version compatibility checks and update timestamps --- internal/cli/acul_app_scaffolding.go | 19 +++++------- internal/cli/acul_screen_scaffolding.go | 40 ++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index bd33f7504..6335bed92 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -51,13 +51,8 @@ type Metadata struct { } // loadManifest loads manifest.json once. -func loadManifest() (*Manifest, error) { - latestTag, err := getLatestReleaseTag() - if err != nil { - return nil, fmt.Errorf("failed to get latest release tag: %w", err) - } - - url := fmt.Sprintf("https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/%s/manifest.json", latestTag) +func loadManifest(tag string) (*Manifest, error) { + url := fmt.Sprintf("https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/%s/manifest.json", tag) resp, err := http.Get(url) if err != nil { @@ -178,7 +173,7 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string, inputs *struct { return fmt.Errorf("failed to get latest release tag: %w", err) } - manifest, err := loadManifest() + manifest, err := loadManifest(latestTag) if err != nil { return err } @@ -470,7 +465,8 @@ func writeAculConfig(destDir, chosenTemplate string, selectedScreens []string, m config := AculConfig{ ChosenTemplate: chosenTemplate, Screens: selectedScreens, - InitTimestamp: time.Now().Format(time.RFC3339), + CreatedAt: time.Now().UTC().Format(time.RFC3339), + ModifiedAt: time.Now().UTC().Format(time.RFC3339), AculManifestVersion: manifestVersion, AppVersion: appVersion, } @@ -617,8 +613,9 @@ func showPostScaffoldingOutput(cli *cli, destDir, successMessage string) { type AculConfig struct { ChosenTemplate string `json:"chosen_template"` Screens []string `json:"screens"` - InitTimestamp string `json:"init_timestamp"` - AppVersion string `json:"app_version,omitempty"` + CreatedAt string `json:"created_at"` + ModifiedAt string `json:"modified_at"` + AppVersion string `json:"app_version"` AculManifestVersion string `json:"acul_manifest_version"` } diff --git a/internal/cli/acul_screen_scaffolding.go b/internal/cli/acul_screen_scaffolding.go index 601a9f99e..af6d2e16e 100644 --- a/internal/cli/acul_screen_scaffolding.go +++ b/internal/cli/acul_screen_scaffolding.go @@ -9,6 +9,7 @@ import ( "log" "os" "path/filepath" + "time" "github.com/spf13/cobra" @@ -54,12 +55,30 @@ func aculScreenAddCmd(cli *cli) *cobra.Command { return cmd } -func scaffoldAddScreen(cli *cli, args []string, destDir string) error { - manifest, err := loadManifest() - if err != nil { - return err +// checkVersionCompatibility compares the user's ACUL config version with the latest available tag +// and warns if the project version is missing or outdated. +func checkVersionCompatibility(cli *cli, aculConfig *AculConfig, latestTag string) { + if aculConfig.AppVersion == "" { + cli.renderer.Warnf( + ansi.Yellow("⚠️ Missing app version in acul_config.json. Reinitialize your project with `auth0 acul init`."), + ) + return + } + + if aculConfig.AppVersion != latestTag { + compareLink := fmt.Sprintf( + "https://github.com/auth0-samples/auth0-acul-samples/compare/%s...%s", + aculConfig.AppVersion, latestTag, + ) + + cli.renderer.Warnf( + ansi.Yellow(fmt.Sprintf("⚠️ ACUL project version outdated (%s). Check updates: %s", + aculConfig.AppVersion, compareLink)), + ) } +} +func scaffoldAddScreen(cli *cli, args []string, destDir string) error { aculConfig, err := loadAculConfig(filepath.Join(destDir, "acul_config.json")) if err != nil { @@ -71,6 +90,18 @@ func scaffoldAddScreen(cli *cli, args []string, destDir string) error { return err } + latestTag, err := getLatestReleaseTag() + if err != nil { + return fmt.Errorf("failed to get latest release tag: %w", err) + } + + manifest, err := loadManifest(aculConfig.AppVersion) + if err != nil { + return err + } + + checkVersionCompatibility(cli, aculConfig, latestTag) + selectedScreens, err := selectAndFilterScreens(cli, args, manifest, aculConfig.ChosenTemplate, aculConfig.Screens) if err != nil { return err @@ -398,6 +429,7 @@ func loadAculConfig(configPath string) (*AculConfig, error) { func updateAculConfigFile(destDir string, aculConfig *AculConfig, selectedScreens []string) error { aculConfig.Screens = append(aculConfig.Screens, selectedScreens...) + aculConfig.ModifiedAt = time.Now().UTC().Format(time.RFC3339) configBytes, err := json.MarshalIndent(aculConfig, "", " ") if err != nil { return fmt.Errorf("failed to marshal updated acul_config.json: %w", err) From bff891bdf0b83a8a27ca056eec1f5ec19bb0d3b8 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Tue, 4 Nov 2025 20:08:51 +0530 Subject: [PATCH 41/58] refactor: streamline screen selection and validation logic in ACUL scaffolding --- docs/auth0_acul_dev.md | 36 +++++++--- internal/cli/acul_app_scaffolding.go | 24 +++---- internal/cli/acul_dev_connected.go | 96 +++++++++++++++---------- internal/cli/acul_screen_scaffolding.go | 23 +++++- 4 files changed, 117 insertions(+), 62 deletions(-) diff --git a/docs/auth0_acul_dev.md b/docs/auth0_acul_dev.md index fd911255b..ca76eb948 100644 --- a/docs/auth0_acul_dev.md +++ b/docs/auth0_acul_dev.md @@ -12,8 +12,20 @@ Start development mode for an ACUL project. This command: - Supports both single screen development and all screens The project directory must contain package.json with a build script. -You need to run your own build process (e.g., npm run build, npm run screen ) -to generate new assets that will be automatically detected and patched. + +DEV MODE (default): +- Requires: --screen flag to specify which screens to develop +- Requires: --port flag for the local development server +- Runs your build process (e.g., npm run screen ) for HMR development + +CONNECTED MODE (--connected): +- Requires: --screen flag to specify screens to patch in Auth0 tenant +- Updates advance rendering settings of the chosen screens in your Auth0 tenant +- Runs initial build and expects you to host assets locally +- Optionally runs build:watch in the background for continuous asset updates +- Watches and patches assets automatically when changes are detected + +⚠️ Connected mode should only be used on stage/dev tenants, not production! ## Usage ``` @@ -23,19 +35,25 @@ auth0 acul dev [flags] ## Examples ``` - auth0 acul dev - auth0 acul dev --dir ./my_acul_project - auth0 acul dev --screen login-id --port 3000 - auth0 acul dev -d ./project -s login-id -p 8080 + # Dev mode + auth0 acul dev --port 3000 + auth0 acul dev --port 8080 + auth0 acul dev -p 8080 --dir ./my_project + + # Connected mode (requires --screen) + auth0 acul dev --connected --screen login-id + auth0 acul dev --connected --screen login-id,signup + auth0 acul dev -c -s login-id -s signup ``` ## Flags ``` - -d, --dir string Path to the ACUL project directory (must contain package.json). - -p, --port string Port for the local development server (default: 8080). (default "8080") - -s, --screen strings Specific screen to develop and watch. If not provided, will watch all screens in the dist/assets folder. + -c, --connected Enable connected mode to update advance rendering settings of Auth0 tenant. Use only on stage/dev tenants. + -d, --dir string Path to the ACUL project directory (must contain package.json). (default ".") + -p, --port string Port for the local development server. + -s, --screen strings Specific screens to develop and watch. Required for both dev and connected modes. Can specify multiple screens. ``` diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index 6335bed92..7d42886e7 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -183,7 +183,12 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string, inputs *struct { return err } - selectedScreens, err := selectScreens(cli, manifest.Templates[chosenTemplate].Screens, inputs.Screens) + var availableScreenIDs []string + for _, s := range manifest.Templates[chosenTemplate].Screens { + availableScreenIDs = append(availableScreenIDs, s.ID) + } + + selectedScreens, err := validateAndSelectScreens(cli, availableScreenIDs, inputs.Screens) if err != nil { return err } @@ -257,17 +262,8 @@ func selectTemplate(cmd *cobra.Command, manifest *Manifest, providedTemplate str return nameToKey[chosenTemplateName], nil } -func selectScreens(cli *cli, screens []Screens, providedScreens []string) ([]string, error) { - return validateAndSelectScreens(cli, screens, providedScreens) -} - // validateAndSelectScreens is a common function for screen validation and selection. -func validateAndSelectScreens(cli *cli, screens []Screens, providedScreens []string) ([]string, error) { - var availableScreenIDs []string - for _, s := range screens { - availableScreenIDs = append(availableScreenIDs, s.ID) - } - +func validateAndSelectScreens(cli *cli, screenIDs []string, providedScreens []string) ([]string, error) { if len(providedScreens) > 0 { var validScreens []string var invalidScreens []string @@ -278,7 +274,7 @@ func validateAndSelectScreens(cli *cli, screens []Screens, providedScreens []str } found := false - for _, availableScreen := range availableScreenIDs { + for _, availableScreen := range screenIDs { if providedScreen == availableScreen { validScreens = append(validScreens, providedScreen) found = true @@ -295,7 +291,7 @@ func validateAndSelectScreens(cli *cli, screens []Screens, providedScreens []str ansi.Bold(ansi.Yellow("⚠️")), ansi.Bold(ansi.Red(strings.Join(invalidScreens, ", ")))) cli.renderer.Infof("Available screens: %s", - ansi.Bold(ansi.Cyan(strings.Join(availableScreenIDs, ", ")))) + ansi.Bold(ansi.Cyan(strings.Join(screenIDs, ", ")))) cli.renderer.Infof("%s We're planning to support all screens in the future.", ansi.Faint("Note:")) } @@ -313,7 +309,7 @@ func validateAndSelectScreens(cli *cli, screens []Screens, providedScreens []str // If no screens provided or no valid screens, prompt for multi-select. var selectedScreens []string - err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, availableScreenIDs...) + err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenIDs...) if len(selectedScreens) == 0 { return nil, fmt.Errorf("at least one screen must be selected") diff --git a/internal/cli/acul_dev_connected.go b/internal/cli/acul_dev_connected.go index e81d76ed7..9683e0a75 100644 --- a/internal/cli/acul_dev_connected.go +++ b/internal/cli/acul_dev_connected.go @@ -11,11 +11,13 @@ import ( "sync" "time" - "github.com/auth0/auth0-cli/internal/ansi" - "github.com/auth0/auth0-cli/internal/prompt" "github.com/auth0/go-auth0/management" "github.com/fsnotify/fsnotify" "github.com/spf13/cobra" + + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/auth0" + "github.com/auth0/auth0-cli/internal/prompt" ) var ( @@ -38,7 +40,7 @@ var ( Name: "Port", LongForm: "port", ShortForm: "p", - Help: "Port for the local development server. Used in dev mode only (default: 8080).", + Help: "Port for the local development server.", IsRequired: false, } connectedFlag = Flag{ @@ -89,11 +91,27 @@ CONNECTED MODE (--connected): auth0 acul dev --connected --screen login-id,signup auth0 acul dev -c -s login-id -s signup`, RunE: func(cmd *cobra.Command, args []string) error { - return runAculDev(cmd.Context(), cli, projectDir, port, screenDirs, connected) + pwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %v", err) + } + + if projectDir == "" { + err = projectDirFlag.Ask(cmd, &projectDir, &pwd) + if err != nil { + return err + } + } + + if err := validateAculProject(projectDir); err != nil { + return fmt.Errorf("invalid ACUL project: %w", err) + } + + return runAculDev(cmd, cli, projectDir, port, screenDirs, connected) }, } - projectDirFlag.RegisterString(cmd, &projectDir, ".") + projectDirFlag.RegisterString(cmd, &projectDir, "") screenDevFlag.RegisterStringSlice(cmd, &screenDirs, nil) portFlag.RegisterString(cmd, &port, "") connectedFlag.RegisterBool(cmd, &connected, false) @@ -101,39 +119,27 @@ CONNECTED MODE (--connected): return cmd } -func runAculDev(ctx context.Context, cli *cli, projectDir, port string, screenDirs []string, connected bool) error { - // TODO: If projDir is empty ensure to speak about defaulting to the current folder which is `.` if they don't want to use the curr Director with the use of dir flag - - if err := validateAculProject(projectDir); err != nil { - return fmt.Errorf("invalid ACUL project: %w", err) - } - +func runAculDev(cmd *cobra.Command, cli *cli, projectDir, port string, screenDirs []string, connected bool) error { if connected { - if len(screenDirs) == 0 { - // ToDO: Prompt to chose from the screens existing in projDir/src/screens by saying the available screens in the proj folder with the use of screens FLag - } + return runConnectedMode(cmd.Context(), cli, projectDir, port, screenDirs) + } - return runConnectedMode(ctx, cli, projectDir, port, screenDirs) - } else { - // Normal dev mode validation - if len(screenDirs) == 0 { - return fmt.Errorf("dev mode requires a screen to be specified with --screen flag") - } - if port == "" { - return fmt.Errorf("dev mode requires a port to be specified with --port flag") + if port == "" { + err := portFlag.Ask(cmd, &projectDir, auth0.String("8080")) + if err != nil { + return err } - return runNormalMode(cli, projectDir, port, screenDirs) } + return runNormalMode(cli, projectDir, screenDirs) } -func runNormalMode(cli *cli, projectDir, port string, screenDirs []string) error { +// ToDo : use the port logic; +func runNormalMode(cli *cli, projectDir string, screenDirs []string) error { fmt.Println(ansi.Bold("🚀 Starting ") + ansi.Cyan("ACUL Dev Mode")) fmt.Printf("📂 Project: %s\n", ansi.Yellow(projectDir)) - // Temporarily fixed port for dev mode - port = "3000" //ToDo : fix the port logic; - fmt.Printf("🖥️ Server: %s\n", ansi.Green(fmt.Sprintf("http://localhost:%s", port))) + fmt.Printf("🖥️ Server: %s\n", ansi.Green(fmt.Sprintf("http://localhost:%s", "3000"))) fmt.Println("💡 " + ansi.Italic("Edit your code and see live changes instantly (HMR enabled)")) screen := screenDirs[0] @@ -347,19 +353,35 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc defer watcher.Close() distAssetsPath := filepath.Join(distPath, "assets") - var screensToWatch []string + var ( + screensToWatch []string + screensInProj []string + ) - if len(screenDirs) == 1 && screenDirs[0] == "all" { - dirs, err := os.ReadDir(distAssetsPath) - if err != nil { - return fmt.Errorf("failed to read assets dir: %w", err) + dirs, err := os.ReadDir(distAssetsPath) + if err != nil { + return fmt.Errorf("failed to read assets dir: %w", err) + } + + for _, d := range dirs { + if d.IsDir() && d.Name() != "shared" { + screensInProj = append(screensInProj, d.Name()) } + } - for _, d := range dirs { - if d.IsDir() && d.Name() != "shared" { - screensToWatch = append(screensToWatch, d.Name()) - } + if len(screensInProj) == 0 { + return fmt.Errorf("no valid screen directories found in dist/assets for the specified screens: %v", screenDirs) + } + + if len(screenDirs) == 0 { + screensToWatch, err = validateAndSelectScreens(cli, screensInProj, screenDirs) + if err != nil { + return err } + } + + if len(screenDirs) == 1 && screenDirs[0] == "all" { + screensToWatch = screensInProj } else { for _, screen := range screenDirs { path := filepath.Join(distAssetsPath, screen) diff --git a/internal/cli/acul_screen_scaffolding.go b/internal/cli/acul_screen_scaffolding.go index af6d2e16e..b024dd1e2 100644 --- a/internal/cli/acul_screen_scaffolding.go +++ b/internal/cli/acul_screen_scaffolding.go @@ -123,7 +123,12 @@ func scaffoldAddScreen(cli *cli, args []string, destDir string) error { } func selectAndFilterScreens(cli *cli, args []string, manifest *Manifest, chosenTemplate string, existingScreens []string) ([]string, error) { - selectedScreens, err := validateAndSelectScreens(cli, manifest.Templates[chosenTemplate].Screens, args) + var availableScreenIDs []string + for _, s := range manifest.Templates[chosenTemplate].Screens { + availableScreenIDs = append(availableScreenIDs, s.ID) + } + + selectedScreens, err := validateAndSelectScreens(cli, availableScreenIDs, args) if err != nil { return nil, err } @@ -427,8 +432,22 @@ func loadAculConfig(configPath string) (*AculConfig, error) { return &config, nil } +func addUniqueScreens(aculConfig *AculConfig, selectedScreens []string) { + existingSet := make(map[string]bool) + for _, screen := range aculConfig.Screens { + existingSet[screen] = true + } + + for _, screen := range selectedScreens { + if !existingSet[screen] { + aculConfig.Screens = append(aculConfig.Screens, screen) + existingSet[screen] = true + } + } +} + func updateAculConfigFile(destDir string, aculConfig *AculConfig, selectedScreens []string) error { - aculConfig.Screens = append(aculConfig.Screens, selectedScreens...) + addUniqueScreens(aculConfig, selectedScreens) aculConfig.ModifiedAt = time.Now().UTC().Format(time.RFC3339) configBytes, err := json.MarshalIndent(aculConfig, "", " ") if err != nil { From 82b2222855dc17d677c7de32da0f226a2424dd24 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Tue, 4 Nov 2025 23:32:47 +0530 Subject: [PATCH 42/58] update ACUL dev mode documentation and improve screen detection logic --- docs/auth0_acul_dev.md | 15 ++-- internal/cli/acul_dev_connected.go | 127 ++++++++++++++++------------- 2 files changed, 80 insertions(+), 62 deletions(-) diff --git a/docs/auth0_acul_dev.md b/docs/auth0_acul_dev.md index ca76eb948..889b57d8d 100644 --- a/docs/auth0_acul_dev.md +++ b/docs/auth0_acul_dev.md @@ -14,7 +14,6 @@ Start development mode for an ACUL project. This command: The project directory must contain package.json with a build script. DEV MODE (default): -- Requires: --screen flag to specify which screens to develop - Requires: --port flag for the local development server - Runs your build process (e.g., npm run screen ) for HMR development @@ -37,13 +36,15 @@ auth0 acul dev [flags] ``` # Dev mode auth0 acul dev --port 3000 - auth0 acul dev --port 8080 auth0 acul dev -p 8080 --dir ./my_project - # Connected mode (requires --screen) + # Connected mode + auth0 acul dev --connected + auth0 acul dev --connected --debug --dir ./my_project + auth0 acul dev --connected --screen all + auth0 acul dev -c --dir ./my_project auth0 acul dev --connected --screen login-id - auth0 acul dev --connected --screen login-id,signup - auth0 acul dev -c -s login-id -s signup + auth0 acul dev -c -s login-id,signup ``` @@ -51,9 +52,9 @@ auth0 acul dev [flags] ``` -c, --connected Enable connected mode to update advance rendering settings of Auth0 tenant. Use only on stage/dev tenants. - -d, --dir string Path to the ACUL project directory (must contain package.json). (default ".") + -d, --dir string Path to the ACUL project directory (must contain package.json). -p, --port string Port for the local development server. - -s, --screen strings Specific screens to develop and watch. Required for both dev and connected modes. Can specify multiple screens. + -s, --screen strings Specific screens to develop and watch. ``` diff --git a/internal/cli/acul_dev_connected.go b/internal/cli/acul_dev_connected.go index 9683e0a75..2380dc024 100644 --- a/internal/cli/acul_dev_connected.go +++ b/internal/cli/acul_dev_connected.go @@ -32,7 +32,7 @@ var ( Name: "Screens", LongForm: "screen", ShortForm: "s", - Help: "Specific screens to develop and watch. Required for both dev and connected modes. Can specify multiple screens.", + Help: "Specific screens to develop and watch.", IsRequired: false, AlwaysPrompt: false, } @@ -69,7 +69,6 @@ func aculDevCmd(cli *cli) *cobra.Command { The project directory must contain package.json with a build script. DEV MODE (default): -- Requires: --screen flag to specify which screens to develop - Requires: --port flag for the local development server - Runs your build process (e.g., npm run screen ) for HMR development @@ -83,13 +82,15 @@ CONNECTED MODE (--connected): ⚠️ Connected mode should only be used on stage/dev tenants, not production!`, Example: ` # Dev mode auth0 acul dev --port 3000 - auth0 acul dev --port 8080 auth0 acul dev -p 8080 --dir ./my_project - # Connected mode (requires --screen) + # Connected mode + auth0 acul dev --connected + auth0 acul dev --connected --debug --dir ./my_project + auth0 acul dev --connected --screen all + auth0 acul dev -c --dir ./my_project auth0 acul dev --connected --screen login-id - auth0 acul dev --connected --screen login-id,signup - auth0 acul dev -c -s login-id -s signup`, + auth0 acul dev -c -s login-id,signup`, RunE: func(cmd *cobra.Command, args []string) error { pwd, err := os.Getwd() if err != nil { @@ -133,7 +134,7 @@ func runAculDev(cmd *cobra.Command, cli *cli, projectDir, port string, screenDir return runNormalMode(cli, projectDir, screenDirs) } -// ToDo : use the port logic; +// ToDo : use the port logic. func runNormalMode(cli *cli, projectDir string, screenDirs []string) error { fmt.Println(ansi.Bold("🚀 Starting ") + ansi.Cyan("ACUL Dev Mode")) @@ -144,12 +145,11 @@ func runNormalMode(cli *cli, projectDir string, screenDirs []string) error { screen := screenDirs[0] - //ToDo: change back to use cmd once run dev command gets supported - // Run npm run dev command + // ToDo: change back to use cmd once run dev command gets supported. Run npm run dev command. cmd := exec.Command("npm", "run", "screen", screen) cmd.Dir = projectDir - // Show output only in debug mode + // Show output only in debug mode. if cli.debug { fmt.Println("\n🔄 Running:", ansi.Cyan(fmt.Sprintf("npm run screen %s", screen))) cmd.Stdout = os.Stdout @@ -163,11 +163,11 @@ func runNormalMode(cli *cli, projectDir string, screenDirs []string) error { return nil } -func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, screenDirs []string) error { +func showConnectedModeInformation() bool { fmt.Println("") fmt.Println("📢 " + ansi.Bold(ansi.Cyan("Connected Mode Information"))) fmt.Println("") - fmt.Println("� " + ansi.Cyan("This mode updates advanced rendering settings for selected screens in your Auth0 tenant.")) + fmt.Println("ℹ️ " + ansi.Cyan("This mode updates advanced rendering settings for selected screens in your Auth0 tenant.")) fmt.Println("🚨 " + ansi.Bold(ansi.Red("IMPORTANT: Never use on production tenants!"))) fmt.Println(" " + ansi.Yellow("Production may break sessions or incur unexpected charges with local assets.")) fmt.Println(" " + ansi.Yellow("Use ONLY for dev/stage tenants.")) @@ -186,17 +186,26 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc fmt.Println(" " + ansi.Cyan("• Watches for changes and automatically re-patches when assets are rebuilt")) fmt.Println("") - if confirmed := prompt.Confirm("Proceed with connected mode?"); !confirmed { + return prompt.Confirm("Proceed with connected mode?") +} + +func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, screenDirs []string) error { + if confirmed := showConnectedModeInformation(); !confirmed { fmt.Println(ansi.Red("❌ Connected mode cancelled.")) return nil } - // Show confirmation banner after user agrees fmt.Println("") fmt.Println("⚠️ " + ansi.Bold(ansi.Yellow("🌟 CONNECTED MODE ENABLED 🌟"))) fmt.Println("") fmt.Println("🚀 " + ansi.Green(fmt.Sprintf("ACUL connected dev mode started for %s", projectDir))) + // Determine screens to watch early after build. + screensToWatch, err := getScreensToWatch(cli, projectDir, screenDirs) + if err != nil { + return fmt.Errorf("failed to determine screens to watch: %w", err) + } + fmt.Println("") fmt.Println("🔨 " + ansi.Bold(ansi.Blue("Step 1: Running initial build..."))) if err := buildProject(cli, projectDir); err != nil { @@ -262,7 +271,7 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc assetsURL := fmt.Sprintf("http://localhost:%s", port) - // Only ask confirmation if not started in background + // Only ask confirmation if not started in background. if !serveStarted { assetsHosted := prompt.Confirm(fmt.Sprintf("Are your assets hosted and accessible at %s?", assetsURL)) if !assetsHosted { @@ -311,12 +320,12 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc distPath := filepath.Join(projectDir, "dist") fmt.Println("🌐 Assets URL: " + ansi.Green(assetsURL)) - fmt.Println("👀 Watching screens: " + ansi.Cyan(strings.Join(screenDirs, ", "))) + fmt.Println("👀 Watching screens: " + ansi.Cyan(strings.Join(screensToWatch, ", "))) fmt.Println("💡 " + ansi.Green("Assets will be patched automatically when changes are detected in the dist folder")) fmt.Println("") fmt.Println("🧪 " + ansi.Bold(ansi.Magenta("Tip: Run 'auth0 test login' to see your changes in action!"))) - return watchAndPatch(ctx, cli, assetsURL, distPath, screenDirs) + return watchAndPatch(ctx, cli, assetsURL, distPath, screensToWatch) } func validateAculProject(projectDir string) error { @@ -327,32 +336,10 @@ func validateAculProject(projectDir string) error { return nil } -func buildProject(cli *cli, projectDir string) error { - cmd := exec.Command("npm", "run", "build") - cmd.Dir = projectDir - - // Only show command output if debug mode is enabled - if cli.debug { - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - } - - if err := cmd.Run(); err != nil { - return fmt.Errorf("build failed: %w", err) - } - - fmt.Println("✅ " + ansi.Green("Build completed successfully")) - return nil -} - -func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screenDirs []string) error { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - defer watcher.Close() +// getScreensToWatch determines which screens to watch based on the provided screenDirs and available screens in the project. +func getScreensToWatch(cli *cli, projectDir string, screenDirs []string) ([]string, error) { + distAssetsPath := filepath.Join(projectDir, "dist", "assets") - distAssetsPath := filepath.Join(distPath, "assets") var ( screensToWatch []string screensInProj []string @@ -360,7 +347,7 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc dirs, err := os.ReadDir(distAssetsPath) if err != nil { - return fmt.Errorf("failed to read assets dir: %w", err) + return nil, fmt.Errorf("failed to read assets dir: %w", err) } for _, d := range dirs { @@ -370,23 +357,23 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } if len(screensInProj) == 0 { - return fmt.Errorf("no valid screen directories found in dist/assets for the specified screens: %v", screenDirs) + return nil, fmt.Errorf("no valid screen directories found in dist/assets for the specified screens: %v", screenDirs) } - if len(screenDirs) == 0 { + switch { + case len(screenDirs) == 0: screensToWatch, err = validateAndSelectScreens(cli, screensInProj, screenDirs) if err != nil { - return err + return nil, err } - } - if len(screenDirs) == 1 && screenDirs[0] == "all" { + case len(screenDirs) == 1 && screenDirs[0] == "all": screensToWatch = screensInProj - } else { + + default: for _, screen := range screenDirs { path := filepath.Join(distAssetsPath, screen) - _, err = os.Stat(path) - if err != nil { + if _, err := os.Stat(path); err != nil { fmt.Println("⚠️ " + ansi.Yellow(fmt.Sprintf("Screen directory '%s' not found in dist/assets: %v", screen, err))) continue } @@ -394,6 +381,34 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } } + return screensToWatch, nil +} + +func buildProject(cli *cli, projectDir string) error { + cmd := exec.Command("npm", "run", "build") + cmd.Dir = projectDir + + // Only show command output if debug mode is enabled. + if cli.debug { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("build failed: %w", err) + } + + fmt.Println("✅ " + ansi.Green("Build completed successfully")) + return nil +} + +func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screensToWatch []string) error { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + defer watcher.Close() + if err := watcher.Add(distPath); err != nil { fmt.Println("⚠️ " + ansi.Yellow("Failed to watch ") + ansi.Bold(distPath) + ": " + err.Error()) } else { @@ -411,11 +426,11 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc return nil } - // React to changes in dist/assets directory + // React to changes in dist/assets directory. if strings.HasSuffix(event.Name, "assets") && event.Op&fsnotify.Create != 0 { now := time.Now() if now.Sub(lastProcessTime) < debounceWindow { - // Only show debounce message in debug mode + // Only show debounce message in debug mode. if cli.debug { cli.renderer.Infof("⏱️ %s", ansi.Yellow("Ignoring event due to debounce window")) } @@ -423,7 +438,7 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } lastProcessTime = now - time.Sleep(500 * time.Millisecond) // let writes settle + time.Sleep(500 * time.Millisecond) // Let writes settle. fmt.Println("📦 " + ansi.Cyan("Change detected in assets. Rebuilding and patching...")) patchAssets(ctx, cli, distPath, assetsURL, screensToWatch, lastHeadTags) @@ -458,7 +473,9 @@ func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, scre return } - fmt.Println("📦 " + ansi.Cyan(fmt.Sprintf("Detected changes for '%s'", screen))) + if cli.debug { + fmt.Println("📦 " + ansi.Cyan(fmt.Sprintf("Detected changes for '%s'", screen))) + } lastHeadTags[screen] = headTags settings := &management.PromptRendering{ From e63bb18c96ce409e0d824e3b53528322b5c9f081 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Wed, 5 Nov 2025 23:50:43 +0530 Subject: [PATCH 43/58] refactor: implement bulk update for screen rendering settings in ACUL --- go.mod | 2 +- go.sum | 4 +- internal/auth0/branding_prompt.go | 5 ++ internal/auth0/mock/branding_prompt_mock.go | 19 ++++++ internal/cli/acul_dev_connected.go | 74 ++++++++++++--------- 5 files changed, 68 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index d96c357e4..5dc98a7aa 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/PuerkitoBio/rehttp v1.4.0 github.com/atotto/clipboard v0.1.4 - github.com/auth0/go-auth0 v1.30.1-0.20251015051405-47f1f1d61b35 + github.com/auth0/go-auth0 v1.31.1-0.20251105130344-77a38d7fca20 github.com/briandowns/spinner v1.23.2 github.com/charmbracelet/glamour v0.10.0 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e diff --git a/go.sum b/go.sum index 4c5b090c0..1cc0868f9 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/auth0/go-auth0 v1.30.1-0.20251015051405-47f1f1d61b35 h1:2XuJRBR1l6ntTquXgUd/yahSqSBoTETXB5enD1TcaZI= -github.com/auth0/go-auth0 v1.30.1-0.20251015051405-47f1f1d61b35/go.mod h1:32sQB1uAn+99fJo6N819EniKq8h785p0ag0lMWhiTaE= +github.com/auth0/go-auth0 v1.31.1-0.20251105130344-77a38d7fca20 h1:KkVNUS1rOcVFuR3eszfclhHYbEYIvoj1OVY+I31rPxQ= +github.com/auth0/go-auth0 v1.31.1-0.20251105130344-77a38d7fca20/go.mod h1:32sQB1uAn+99fJo6N819EniKq8h785p0ag0lMWhiTaE= github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5rljDQLKUO+cRJCnduDyn11+zGZIc9Z48= github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0/go.mod h1:6L7zgvqo0idzI7IO8de6ZC051AfXb5ipkIJ7bIA2tGA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= diff --git a/internal/auth0/branding_prompt.go b/internal/auth0/branding_prompt.go index 8a6bafc1a..329cc72a2 100644 --- a/internal/auth0/branding_prompt.go +++ b/internal/auth0/branding_prompt.go @@ -49,6 +49,11 @@ type PromptAPI interface { // See: https://auth0.com/docs/api/management/v2/prompts/patch-rendering UpdateRendering(ctx context.Context, prompt management.PromptType, screen management.ScreenName, c *management.PromptRendering, opts ...management.RequestOption) error + // BulkUpdateRendering updates multiple rendering settings in a single operation. + // + // See: https://auth0.com/docs/api/management/v2/prompts/patch-bulk-rendering + BulkUpdateRendering(ctx context.Context, c *management.PromptRenderingUpdateRequest, opts ...management.RequestOption) error + // ListRendering retrieves the settings for the ACUL. // ListRendering(ctx context.Context, opts ...management.RequestOption) (c *management.PromptRenderingList, err error) diff --git a/internal/auth0/mock/branding_prompt_mock.go b/internal/auth0/mock/branding_prompt_mock.go index 08a4754c5..1b53376dd 100644 --- a/internal/auth0/mock/branding_prompt_mock.go +++ b/internal/auth0/mock/branding_prompt_mock.go @@ -35,6 +35,25 @@ func (m *MockPromptAPI) EXPECT() *MockPromptAPIMockRecorder { return m.recorder } +// BulkUpdateRendering mocks base method. +func (m *MockPromptAPI) BulkUpdateRendering(ctx context.Context, c *management.PromptRenderingUpdateRequest, opts ...management.RequestOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, c} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "BulkUpdateRendering", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// BulkUpdateRendering indicates an expected call of BulkUpdateRendering. +func (mr *MockPromptAPIMockRecorder) BulkUpdateRendering(ctx, c interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, c}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BulkUpdateRendering", reflect.TypeOf((*MockPromptAPI)(nil).BulkUpdateRendering), varargs...) +} + // CustomText mocks base method. func (m *MockPromptAPI) CustomText(ctx context.Context, p, l string, opts ...management.RequestOption) (map[string]interface{}, error) { m.ctrl.T.Helper() diff --git a/internal/cli/acul_dev_connected.go b/internal/cli/acul_dev_connected.go index 2380dc024..15a835390 100644 --- a/internal/cli/acul_dev_connected.go +++ b/internal/cli/acul_dev_connected.go @@ -8,7 +8,6 @@ import ( "path/filepath" "reflect" "strings" - "sync" "time" "github.com/auth0/go-auth0/management" @@ -454,50 +453,59 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, screensToWatch []string, lastHeadTags map[string][]interface{}) { - var wg sync.WaitGroup - errChan := make(chan error, len(screensToWatch)) + var promptRenderings []*management.PromptRendering + var updatedScreens []string for _, screen := range screensToWatch { - wg.Add(1) - go func(screen string) { - defer wg.Done() + headTags, err := buildHeadTagsFromDirs(distPath, assetsURL, screen) + if err != nil { + fmt.Println("⚠️ " + ansi.Yellow("Failed to build headTags for ") + ansi.Bold(screen) + ": " + err.Error()) + continue + } - headTags, err := buildHeadTagsFromDirs(distPath, assetsURL, screen) - if err != nil { - errChan <- fmt.Errorf("failed to build headTags for %s: %w", screen, err) - return - } + if reflect.DeepEqual(lastHeadTags[screen], headTags) { + fmt.Println("🔁 " + ansi.Cyan(fmt.Sprintf("Skipping patch for '%s' — headTags unchanged", screen))) + continue + } - if reflect.DeepEqual(lastHeadTags[screen], headTags) { - fmt.Println("🔁 " + ansi.Cyan(fmt.Sprintf("Skipping patch for '%s' — headTags unchanged", screen))) - return - } + if cli.debug { + fmt.Println("📦 " + ansi.Cyan(fmt.Sprintf("Detected changes for '%s'", screen))) + } + lastHeadTags[screen] = headTags - if cli.debug { - fmt.Println("📦 " + ansi.Cyan(fmt.Sprintf("Detected changes for '%s'", screen))) - } - lastHeadTags[screen] = headTags + promptType := management.PromptType(ScreenPromptMap[screen]) + screenName := management.ScreenName(screen) - settings := &management.PromptRendering{ - RenderingMode: &management.RenderingModeAdvanced, - HeadTags: headTags, - } + settings := &management.PromptRendering{ + Prompt: &promptType, + Screen: &screenName, + RenderingMode: &management.RenderingModeAdvanced, + HeadTags: headTags, + } - if err = cli.api.Prompt.UpdateRendering(ctx, management.PromptType(ScreenPromptMap[screen]), management.ScreenName(screen), settings); err != nil { - errChan <- fmt.Errorf("failed to patch settings for %s: %w", screen, err) - return - } + promptRenderings = append(promptRenderings, settings) + updatedScreens = append(updatedScreens, screen) + } - fmt.Println("✅ " + ansi.Green(fmt.Sprintf("Successfully patched screen '%s'", screen))) - }(screen) + // If no screens need updating, return early. + if len(promptRenderings) == 0 { + if cli.debug { + fmt.Println("🔁 " + ansi.Cyan("No screens require updates")) + } + return } - wg.Wait() - close(errChan) + bulkRequest := &management.PromptRenderingUpdateRequest{ + PromptRenderings: promptRenderings, + } - for err := range errChan { - fmt.Println("⚠️ " + ansi.Yellow("Patch error: ") + ansi.Bold(err.Error())) + if err := cli.api.Prompt.BulkUpdateRendering(ctx, bulkRequest); err != nil { + fmt.Println("⚠️ " + ansi.Yellow("Bulk patch error: ") + ansi.Bold(err.Error())) + return } + + // Report success for all updated screens. + fmt.Println("✅ " + ansi.Green(fmt.Sprintf("Successfully patched screen '%s'", updatedScreens))) } func buildHeadTagsFromDirs(distPath, assetsURL, screen string) ([]interface{}, error) { From 0900986d0a104a5fafb9dadd7fd1059459a806ba Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Thu, 6 Nov 2025 02:06:57 +0530 Subject: [PATCH 44/58] enhance screen selection and validation logic in ACUL dev mode --- internal/cli/acul_dev_connected.go | 303 +++++++++++++++++++---------- 1 file changed, 197 insertions(+), 106 deletions(-) diff --git a/internal/cli/acul_dev_connected.go b/internal/cli/acul_dev_connected.go index 15a835390..97b28a439 100644 --- a/internal/cli/acul_dev_connected.go +++ b/internal/cli/acul_dev_connected.go @@ -121,7 +121,12 @@ CONNECTED MODE (--connected): func runAculDev(cmd *cobra.Command, cli *cli, projectDir, port string, screenDirs []string, connected bool) error { if connected { - return runConnectedMode(cmd.Context(), cli, projectDir, port, screenDirs) + screensToWatch, err := selectScreensSimple(cli, projectDir, screenDirs) + if err != nil { + return fmt.Errorf("failed to determine screens to watch: %w", err) + } + + return runConnectedMode(cmd.Context(), cli, projectDir, port, screensToWatch) } if port == "" { @@ -135,6 +140,7 @@ func runAculDev(cmd *cobra.Command, cli *cli, projectDir, port string, screenDir // ToDo : use the port logic. func runNormalMode(cli *cli, projectDir string, screenDirs []string) error { + var screen string fmt.Println(ansi.Bold("🚀 Starting ") + ansi.Cyan("ACUL Dev Mode")) fmt.Printf("📂 Project: %s\n", ansi.Yellow(projectDir)) @@ -142,9 +148,15 @@ func runNormalMode(cli *cli, projectDir string, screenDirs []string) error { fmt.Printf("🖥️ Server: %s\n", ansi.Green(fmt.Sprintf("http://localhost:%s", "3000"))) fmt.Println("💡 " + ansi.Italic("Edit your code and see live changes instantly (HMR enabled)")) - screen := screenDirs[0] + if len(screenDirs) == 0 { + screen = "login-id" + // ToDo: change back to use cmd once run dev command gets supported. Run npm run dev command. + fmt.Println("Defaulting to running 'npm run screen login-id' for dev mode...") + } else { + screen = screenDirs[0] + fmt.Println("Running 'npm run screen " + screen + "' for dev mode...") + } - // ToDo: change back to use cmd once run dev command gets supported. Run npm run dev command. cmd := exec.Command("npm", "run", "screen", screen) cmd.Dir = projectDir @@ -188,7 +200,7 @@ func showConnectedModeInformation() bool { return prompt.Confirm("Proceed with connected mode?") } -func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, screenDirs []string) error { +func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, screensToWatch []string) error { if confirmed := showConnectedModeInformation(); !confirmed { fmt.Println(ansi.Red("❌ Connected mode cancelled.")) return nil @@ -199,18 +211,18 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc fmt.Println("") fmt.Println("🚀 " + ansi.Green(fmt.Sprintf("ACUL connected dev mode started for %s", projectDir))) - // Determine screens to watch early after build. - screensToWatch, err := getScreensToWatch(cli, projectDir, screenDirs) - if err != nil { - return fmt.Errorf("failed to determine screens to watch: %w", err) - } - fmt.Println("") fmt.Println("🔨 " + ansi.Bold(ansi.Blue("Step 1: Running initial build..."))) if err := buildProject(cli, projectDir); err != nil { return fmt.Errorf("initial build failed: %w", err) } + // Always validate screens after build to ensure they have actual built assets. + screensToWatch, err := validateScreensAfterBuild(projectDir, screensToWatch) + if err != nil { + return fmt.Errorf("screen validation failed after build: %w", err) + } + fmt.Println("") fmt.Println("📡 " + ansi.Bold(ansi.Blue("Step 2: Host your assets locally"))) @@ -255,6 +267,7 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc serveStarted = true fmt.Println("✅ " + ansi.Green("Local server started successfully at ") + ansi.Cyan(fmt.Sprintf("http://localhost:%s", port))) + time.Sleep(2 * time.Second) // give server time to start. defer func() { if serveCmd.Process != nil { serveCmd.Process.Kill() @@ -335,76 +348,144 @@ func validateAculProject(projectDir string) error { return nil } -// getScreensToWatch determines which screens to watch based on the provided screenDirs and available screens in the project. -func getScreensToWatch(cli *cli, projectDir string, screenDirs []string) ([]string, error) { +func buildProject(cli *cli, projectDir string) error { + cmd := exec.Command("npm", "run", "build") + cmd.Dir = projectDir + + // Only show command output if debug mode is enabled. + if cli.debug { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("build failed: %w", err) + } + + fmt.Println("✅ " + ansi.Green("Build completed successfully")) + return nil +} + +func selectScreensSimple(cli *cli, projectDir string, screenDirs []string) ([]string, error) { + // 1. Screens provided via --screen flag. + if len(screenDirs) > 0 { + if len(screenDirs) == 1 && screenDirs[0] == "all" { + cli.renderer.Infof(ansi.Cyan("📂 Selecting all screens from src/screens")) + + return getScreensFromSrcFolder(filepath.Join(projectDir, "src", "screens")) + } + + cli.renderer.Infof(ansi.Cyan(fmt.Sprintf("📂 Using specified screens: %s", strings.Join(screenDirs, ", ")))) + + return screenDirs, nil + } + + // 2. No --screen flag: auto-detect from src/screens. + srcScreensPath := filepath.Join(projectDir, "src", "screens") + + if availableScreens, err := getScreensFromSrcFolder(srcScreensPath); err == nil && len(availableScreens) > 0 { + cli.renderer.Infof(ansi.Cyan(fmt.Sprintf("📂 Detected screens in src/screens: %s", strings.Join(availableScreens, ", ")))) + + return validateAndSelectScreens(cli, availableScreens, nil) + } + + return nil, fmt.Errorf(`no screens found in project. + +Please either: +1. Specify screens using --screen flag: auth0 acul dev --connected --screen login-id,signup +2. Create a new ACUL project: auth0 acul init +3. Ensure your project has screens in src/screens/ folder`) +} + +func validateScreensAfterBuild(projectDir string, selectedScreens []string) ([]string, error) { distAssetsPath := filepath.Join(projectDir, "dist", "assets") - var ( - screensToWatch []string - screensInProj []string - ) + availableScreens, err := getScreensFromDistAssets(distAssetsPath) - dirs, err := os.ReadDir(distAssetsPath) if err != nil { - return nil, fmt.Errorf("failed to read assets dir: %w", err) + return nil, fmt.Errorf("failed to read available screens from dist/assets: %w", err) } - for _, d := range dirs { - if d.IsDir() && d.Name() != "shared" { - screensInProj = append(screensInProj, d.Name()) - } + if len(availableScreens) == 0 { + return nil, fmt.Errorf("no valid screens found in dist/assets after build") } - if len(screensInProj) == 0 { - return nil, fmt.Errorf("no valid screen directories found in dist/assets for the specified screens: %v", screenDirs) + availableScreensMap := make(map[string]bool) + + for _, screen := range availableScreens { + availableScreensMap[screen] = true } - switch { - case len(screenDirs) == 0: - screensToWatch, err = validateAndSelectScreens(cli, screensInProj, screenDirs) - if err != nil { - return nil, err + var validScreens, missingScreens []string + + for _, screen := range selectedScreens { + if availableScreensMap[screen] { + validScreens = append(validScreens, screen) + } else { + missingScreens = append(missingScreens, screen) } + } - case len(screenDirs) == 1 && screenDirs[0] == "all": - screensToWatch = screensInProj + if len(missingScreens) > 0 { + return nil, fmt.Errorf("⚠️ Missing built assets for: %s", strings.Join(missingScreens, ", ")) + } - default: - for _, screen := range screenDirs { - path := filepath.Join(distAssetsPath, screen) - if _, err := os.Stat(path); err != nil { - fmt.Println("⚠️ " + ansi.Yellow(fmt.Sprintf("Screen directory '%s' not found in dist/assets: %v", screen, err))) - continue - } - screensToWatch = append(screensToWatch, screen) + if len(validScreens) == 0 { + return nil, fmt.Errorf( + "none of the selected screens were built. Available built screens: %s", + strings.Join(availableScreens, ", "), + ) + } + + return validScreens, nil +} + +// getScreensFromDistAssets reads screen names from dist/assets folder. +func getScreensFromDistAssets(distAssetsPath string) ([]string, error) { + if _, err := os.Stat(distAssetsPath); os.IsNotExist(err) { + return nil, fmt.Errorf("dist/assets not found") + } + + dirs, err := os.ReadDir(distAssetsPath) + if err != nil { + return nil, fmt.Errorf("failed to read dist/assets: %w", err) + } + + var screens []string + for _, d := range dirs { + if d.IsDir() && d.Name() != "shared" { + screens = append(screens, d.Name()) } } - return screensToWatch, nil + return screens, nil } -func buildProject(cli *cli, projectDir string) error { - cmd := exec.Command("npm", "run", "build") - cmd.Dir = projectDir +// getScreensFromSrcFolder reads screen names from src/screens folder. +func getScreensFromSrcFolder(srcScreensPath string) ([]string, error) { + if _, err := os.Stat(srcScreensPath); os.IsNotExist(err) { + return nil, fmt.Errorf("src/screens not found") + } - // Only show command output if debug mode is enabled. - if cli.debug { - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + entries, err := os.ReadDir(srcScreensPath) + if err != nil { + return nil, fmt.Errorf("failed to read src/screens: %w", err) } - if err := cmd.Run(); err != nil { - return fmt.Errorf("build failed: %w", err) + var screens []string + for _, entry := range entries { + if entry.IsDir() { + screens = append(screens, entry.Name()) + } } - fmt.Println("✅ " + ansi.Green("Build completed successfully")) - return nil + return screens, nil } func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screensToWatch []string) error { watcher, err := fsnotify.NewWatcher() if err != nil { - return err + return fmt.Errorf("failed to create watcher: %w", err) } defer watcher.Close() @@ -415,7 +496,7 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } const debounceWindow = 5 * time.Second - var lastProcessTime time.Time + var lastEventTime time.Time lastHeadTags := make(map[string][]interface{}) for { @@ -425,26 +506,29 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc return nil } - // React to changes in dist/assets directory. - if strings.HasSuffix(event.Name, "assets") && event.Op&fsnotify.Create != 0 { - now := time.Now() - if now.Sub(lastProcessTime) < debounceWindow { - // Only show debounce message in debug mode. - if cli.debug { - cli.renderer.Infof("⏱️ %s", ansi.Yellow("Ignoring event due to debounce window")) - } - continue + // Trigger only on changes inside dist/assets/ + if !strings.Contains(event.Name, "assets") { + continue + } + + now := time.Now() + if now.Sub(lastEventTime) < debounceWindow { + if cli.debug { + fmt.Println(ansi.Yellow("⏱️ Skipping duplicate event (debounce window)")) } - lastProcessTime = now + continue + } + lastEventTime = now - time.Sleep(500 * time.Millisecond) // Let writes settle. - fmt.Println("📦 " + ansi.Cyan("Change detected in assets. Rebuilding and patching...")) + time.Sleep(500 * time.Millisecond) // let writes settle + fmt.Println(ansi.Cyan("📦 Change detected — rebuilding and patching assets...")) - patchAssets(ctx, cli, distPath, assetsURL, screensToWatch, lastHeadTags) + if err := patchAssets(ctx, cli, distPath, assetsURL, screensToWatch, lastHeadTags); err != nil { + cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf("⚠️ Patch failed: %v", err))) } case err := <-watcher.Errors: - fmt.Println("⚠️ " + ansi.Yellow("Watcher error: ") + ansi.Bold(err.Error())) + cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf("⚠️ Watcher error: %v", err))) case <-ctx.Done(): return ctx.Err() @@ -452,88 +536,92 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } } -func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, screensToWatch []string, lastHeadTags map[string][]interface{}) { - var promptRenderings []*management.PromptRendering - var updatedScreens []string +func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, screensToWatch []string, lastHeadTags map[string][]interface{}) error { + var ( + renderings []*management.PromptRendering + updated []string + ) for _, screen := range screensToWatch { headTags, err := buildHeadTagsFromDirs(distPath, assetsURL, screen) if err != nil { - fmt.Println("⚠️ " + ansi.Yellow("Failed to build headTags for ") + ansi.Bold(screen) + ": " + err.Error()) + if cli.debug { + fmt.Println("⚠️ " + ansi.Yellow(fmt.Sprintf("Skipping '%s': %v", screen, err))) + } continue } if reflect.DeepEqual(lastHeadTags[screen], headTags) { - fmt.Println("🔁 " + ansi.Cyan(fmt.Sprintf("Skipping patch for '%s' — headTags unchanged", screen))) + if cli.debug { + fmt.Println("🔁 " + ansi.Cyan(fmt.Sprintf("No changes detected for '%s'", screen))) + } continue } - - if cli.debug { - fmt.Println("📦 " + ansi.Cyan(fmt.Sprintf("Detected changes for '%s'", screen))) - } lastHeadTags[screen] = headTags promptType := management.PromptType(ScreenPromptMap[screen]) - screenName := management.ScreenName(screen) + screenType := management.ScreenName(screen) - settings := &management.PromptRendering{ + renderings = append(renderings, &management.PromptRendering{ Prompt: &promptType, - Screen: &screenName, + Screen: &screenType, RenderingMode: &management.RenderingModeAdvanced, HeadTags: headTags, - } - - promptRenderings = append(promptRenderings, settings) - updatedScreens = append(updatedScreens, screen) + }) + updated = append(updated, screen) } - // If no screens need updating, return early. - if len(promptRenderings) == 0 { + if len(renderings) == 0 { if cli.debug { - fmt.Println("🔁 " + ansi.Cyan("No screens require updates")) + cli.renderer.Infof(ansi.Cyan("🔁 No screens to patch")) } - return + return nil } - bulkRequest := &management.PromptRenderingUpdateRequest{ - PromptRenderings: promptRenderings, + req := &management.PromptRenderingUpdateRequest{PromptRenderings: renderings} + if err := cli.api.Prompt.BulkUpdateRendering(ctx, req); err != nil { + return fmt.Errorf("bulk patch error: %w", err) } - if err := cli.api.Prompt.BulkUpdateRendering(ctx, bulkRequest); err != nil { - fmt.Println("⚠️ " + ansi.Yellow("Bulk patch error: ") + ansi.Bold(err.Error())) - return + if len(updated) == 1 { + fmt.Println(ansi.Green(fmt.Sprintf("✅ Patched screen: %s", updated[0]))) + } else { + fmt.Println(ansi.Green(fmt.Sprintf("✅ Patched %d screens: %s", len(updated), strings.Join(updated, ", ")))) } - // Report success for all updated screens. - fmt.Println("✅ " + ansi.Green(fmt.Sprintf("Successfully patched screen '%s'", updatedScreens))) + return nil } func buildHeadTagsFromDirs(distPath, assetsURL, screen string) ([]interface{}, error) { - var tags []interface{} - screenPath := filepath.Join(distPath, "assets", screen) - sharedPath := filepath.Join(distPath, "assets", "shared") - mainPath := filepath.Join(distPath, "assets") - sources := []string{sharedPath, screenPath, mainPath} + searchDirs := []string{ + filepath.Join(distPath, "assets", "shared"), + filepath.Join(distPath, "assets", screen), + filepath.Join(distPath, "assets"), + } - for _, dir := range sources { + var tags []interface{} + for _, dir := range searchDirs { entries, err := os.ReadDir(dir) if err != nil { continue } - for _, entry := range entries { - if entry.IsDir() { + + for _, e := range entries { + if e.IsDir() { continue } - name := entry.Name() + + ext := filepath.Ext(e.Name()) subDir := filepath.Base(dir) if subDir == "assets" { subDir = "" } - src := fmt.Sprintf("%s/assets/%s%s", assetsURL, subDir, name) + + src := fmt.Sprintf("%s/assets", assetsURL) if subDir != "" { - src = fmt.Sprintf("%s/assets/%s/%s", assetsURL, subDir, name) + src = fmt.Sprintf("%s/%s", src, subDir) } - ext := filepath.Ext(name) + src = fmt.Sprintf("%s/%s", src, e.Name()) switch ext { case ".js": @@ -556,5 +644,8 @@ func buildHeadTagsFromDirs(distPath, assetsURL, screen string) ([]interface{}, e } } } + if len(tags) == 0 { + return nil, fmt.Errorf("no .js or .css assets found for '%s'", screen) + } return tags, nil } From 5e1d1a4a5e6eaa551915e13815805b324aec03c0 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Thu, 6 Nov 2025 16:14:30 +0530 Subject: [PATCH 45/58] enhance connected mode feedback and streamline output messages in ACUL --- internal/cli/acul_dev_connected.go | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/internal/cli/acul_dev_connected.go b/internal/cli/acul_dev_connected.go index 97b28a439..5bb9872fa 100644 --- a/internal/cli/acul_dev_connected.go +++ b/internal/cli/acul_dev_connected.go @@ -121,6 +121,15 @@ CONNECTED MODE (--connected): func runAculDev(cmd *cobra.Command, cli *cli, projectDir, port string, screenDirs []string, connected bool) error { if connected { + if confirmed := showConnectedModeInformation(); !confirmed { + fmt.Println(ansi.Red("❌ Connected mode cancelled.")) + return nil + } + + fmt.Println("") + fmt.Println("⚠️ " + ansi.Bold(ansi.Yellow("🌟 CONNECTED MODE ENABLED 🌟"))) + fmt.Println("") + screensToWatch, err := selectScreensSimple(cli, projectDir, screenDirs) if err != nil { return fmt.Errorf("failed to determine screens to watch: %w", err) @@ -201,14 +210,6 @@ func showConnectedModeInformation() bool { } func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, screensToWatch []string) error { - if confirmed := showConnectedModeInformation(); !confirmed { - fmt.Println(ansi.Red("❌ Connected mode cancelled.")) - return nil - } - - fmt.Println("") - fmt.Println("⚠️ " + ansi.Bold(ansi.Yellow("🌟 CONNECTED MODE ENABLED 🌟"))) - fmt.Println("") fmt.Println("🚀 " + ansi.Green(fmt.Sprintf("ACUL connected dev mode started for %s", projectDir))) fmt.Println("") @@ -267,7 +268,7 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc serveStarted = true fmt.Println("✅ " + ansi.Green("Local server started successfully at ") + ansi.Cyan(fmt.Sprintf("http://localhost:%s", port))) - time.Sleep(2 * time.Second) // give server time to start. + time.Sleep(2 * time.Second) // Give server time to start. defer func() { if serveCmd.Process != nil { serveCmd.Process.Kill() @@ -506,7 +507,7 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc return nil } - // Trigger only on changes inside dist/assets/ + // Trigger only on changes inside dist/assets/. if !strings.Contains(event.Name, "assets") { continue } @@ -520,7 +521,7 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc } lastEventTime = now - time.Sleep(500 * time.Millisecond) // let writes settle + time.Sleep(500 * time.Millisecond) // Let writes settle. fmt.Println(ansi.Cyan("📦 Change detected — rebuilding and patching assets...")) if err := patchAssets(ctx, cli, distPath, assetsURL, screensToWatch, lastHeadTags); err != nil { @@ -582,12 +583,7 @@ func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, scre if err := cli.api.Prompt.BulkUpdateRendering(ctx, req); err != nil { return fmt.Errorf("bulk patch error: %w", err) } - - if len(updated) == 1 { - fmt.Println(ansi.Green(fmt.Sprintf("✅ Patched screen: %s", updated[0]))) - } else { - fmt.Println(ansi.Green(fmt.Sprintf("✅ Patched %d screens: %s", len(updated), strings.Join(updated, ", ")))) - } + fmt.Println(ansi.Green(fmt.Sprintf("✅ Patched %d screen(s): %s", len(updated), strings.Join(updated, ", ")))) return nil } From 3b0cebfc5a3899999f60164520d80fa1327ac641 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Thu, 6 Nov 2025 19:44:19 +0530 Subject: [PATCH 46/58] enhance asset patching and restore original settings on shutdown in ACUL --- internal/cli/acul_dev_connected.go | 162 +++++++++++++++++++++++------ 1 file changed, 133 insertions(+), 29 deletions(-) diff --git a/internal/cli/acul_dev_connected.go b/internal/cli/acul_dev_connected.go index 5bb9872fa..d3637496d 100644 --- a/internal/cli/acul_dev_connected.go +++ b/internal/cli/acul_dev_connected.go @@ -5,9 +5,11 @@ import ( "fmt" "os" "os/exec" + "os/signal" "path/filepath" "reflect" "strings" + "syscall" "time" "github.com/auth0/go-auth0/management" @@ -269,11 +271,6 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc fmt.Println("✅ " + ansi.Green("Local server started successfully at ") + ansi.Cyan(fmt.Sprintf("http://localhost:%s", port))) time.Sleep(2 * time.Second) // Give server time to start. - defer func() { - if serveCmd.Process != nil { - serveCmd.Process.Kill() - } - }() } } else { fmt.Println("📋 " + ansi.Cyan("Please host your assets manually using:")) @@ -304,7 +301,7 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc var buildWatchCmd *exec.Cmd if runBuildWatch { - fmt.Println("🔄 " + ansi.Cyan("Starting 'npm run build:watch' in the background...")) + fmt.Println("🚀 " + ansi.Cyan("Starting 'npm run build:watch' in the background...")) buildWatchCmd = exec.Command("npm", "run", "build:watch") buildWatchCmd.Dir = projectDir @@ -316,14 +313,9 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc if err := buildWatchCmd.Start(); err != nil { fmt.Println("⚠️ " + ansi.Yellow("Failed to start build:watch: ") + ansi.Bold(err.Error())) - fmt.Println(" You can manually run " + ansi.Cyan("'npm run build'") + " when changes are made.") + fmt.Println(" You can manually run " + ansi.Cyan("'npm run build'") + " whenever you update your code.") } else { fmt.Println("✅ " + ansi.Green("Build watch started successfully")) - defer func() { - if buildWatchCmd.Process != nil { - buildWatchCmd.Process.Kill() - } - }() } } @@ -334,11 +326,25 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc fmt.Println("🌐 Assets URL: " + ansi.Green(assetsURL)) fmt.Println("👀 Watching screens: " + ansi.Cyan(strings.Join(screensToWatch, ", "))) + + // Fetch original head tags before starting watcher. + fmt.Println("💡 " + ansi.Cyan("Fetching original rendering settings for restoration on exit...")) + originalHeadTags, err := fetchOriginalHeadTags(ctx, cli, screensToWatch) + if err != nil { + fmt.Println("⚠️ " + ansi.Yellow(fmt.Sprintf("Warning: Could not fetch original settings: %v", err))) + fmt.Println(" " + ansi.Yellow("Original settings will not be restored on exit.")) + originalHeadTags = nil // Continue without restoration capability. + } else { + fmt.Println("✅ " + ansi.Green(fmt.Sprintf("Original settings saved for %d screen(s)", len(originalHeadTags)))) + } + + fmt.Println("") fmt.Println("💡 " + ansi.Green("Assets will be patched automatically when changes are detected in the dist folder")) fmt.Println("") - fmt.Println("🧪 " + ansi.Bold(ansi.Magenta("Tip: Run 'auth0 test login' to see your changes in action!"))) + fmt.Println(ansi.Bold(ansi.Magenta("Tip: Run 'auth0 test login' to see your changes in action!"))) + fmt.Println(ansi.Cyan("Press Ctrl+C to stop and restore original settings")) - return watchAndPatch(ctx, cli, assetsURL, distPath, screensToWatch) + return watchAndPatch(ctx, cli, assetsURL, distPath, screensToWatch, buildWatchCmd, serveCmd, serveStarted, originalHeadTags) } func validateAculProject(projectDir string) error { @@ -483,7 +489,66 @@ func getScreensFromSrcFolder(srcScreensPath string) ([]string, error) { return screens, nil } -func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screensToWatch []string) error { +// fetchOriginalHeadTags retrieves the current rendering settings for all screens before making changes. +func fetchOriginalHeadTags(ctx context.Context, cli *cli, screensToWatch []string) (map[string][]interface{}, error) { + originalTags := make(map[string][]interface{}) + + for _, screen := range screensToWatch { + promptType := management.PromptType(ScreenPromptMap[screen]) + screenType := management.ScreenName(screen) + + rendering, err := cli.api.Prompt.ReadRendering(ctx, promptType, screenType) + if err != nil { + if cli.debug { + fmt.Println("⚠️ " + ansi.Yellow(fmt.Sprintf("Could not fetch original settings for '%s': %v", screen, err))) + } + continue + } + + if rendering != nil && rendering.HeadTags != nil { + originalTags[screen] = rendering.HeadTags + if cli.debug { + fmt.Println("📥 " + ansi.Cyan(fmt.Sprintf("Saved original settings for '%s' (%d tags)", screen, len(rendering.HeadTags)))) + } + } + } + + return originalTags, nil +} + +// restoreOriginalHeadTags restores the original rendering settings that were saved at startup. +func restoreOriginalHeadTags(ctx context.Context, cli *cli, originalHeadTags map[string][]interface{}) error { + var renderings []*management.PromptRendering + + for screen, headTags := range originalHeadTags { + promptType := management.PromptType(ScreenPromptMap[screen]) + screenType := management.ScreenName(screen) + + renderings = append(renderings, &management.PromptRendering{ + Prompt: &promptType, + Screen: &screenType, + RenderingMode: &management.RenderingModeAdvanced, + HeadTags: headTags, + }) + + if cli.debug { + fmt.Fprintln(os.Stderr, fmt.Sprintf(" 🔄 Restoring '%s' with %d tags", screen, len(headTags))) + } + } + + if len(renderings) == 0 { + return fmt.Errorf("no original settings to restore") + } + + req := &management.PromptRenderingUpdateRequest{PromptRenderings: renderings} + if err := cli.api.Prompt.BulkUpdateRendering(ctx, req); err != nil { + return fmt.Errorf("bulk restore error: %w", err) + } + + return nil +} + +func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screensToWatch []string, buildWatchCmd, serveCmd *exec.Cmd, serveStarted bool, originalHeadTags map[string][]interface{}) error { watcher, err := fsnotify.NewWatcher() if err != nil { return fmt.Errorf("failed to create watcher: %w", err) @@ -491,11 +556,19 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc defer watcher.Close() if err := watcher.Add(distPath); err != nil { - fmt.Println("⚠️ " + ansi.Yellow("Failed to watch ") + ansi.Bold(distPath) + ": " + err.Error()) - } else { - fmt.Println("👀 Watching: " + ansi.Cyan(fmt.Sprintf("%d screen(s): %v", len(screensToWatch), screensToWatch))) + cli.renderer.Warnf("Failed to watch %s: %v", distPath, err) } + cli.renderer.Infof("Watching: %s", strings.Join(screensToWatch, ", ")) + + // Signal handling for graceful shutdown + // First, stop any existing global signal handlers (from root.go) + signal.Reset(os.Interrupt, syscall.SIGTERM) + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + defer signal.Stop(sigChan) // Clean up signal handler when function exits + const debounceWindow = 5 * time.Second var lastEventTime time.Time lastHeadTags := make(map[string][]interface{}) @@ -514,22 +587,52 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc now := time.Now() if now.Sub(lastEventTime) < debounceWindow { - if cli.debug { - fmt.Println(ansi.Yellow("⏱️ Skipping duplicate event (debounce window)")) - } continue } lastEventTime = now time.Sleep(500 * time.Millisecond) // Let writes settle. - fmt.Println(ansi.Cyan("📦 Change detected — rebuilding and patching assets...")) - + cli.renderer.Warnf(ansi.Cyan("Change detected, patching assets...")) if err := patchAssets(ctx, cli, distPath, assetsURL, screensToWatch, lastHeadTags); err != nil { - cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf("⚠️ Patch failed: %v", err))) + cli.renderer.Errorf("Patch failed: %v", err) } case err := <-watcher.Errors: - cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf("⚠️ Watcher error: %v", err))) + cli.renderer.Warnf("Watcher error: %v", err) + + case <-sigChan: + fmt.Fprintln(os.Stderr, "\nShutdown signal received, cleaning up...") + + // Restore original head tags if available + if originalHeadTags != nil && len(originalHeadTags) > 0 { + fmt.Fprintln(os.Stderr, "Restoring original settings...") + + if err := restoreOriginalHeadTags(ctx, cli, originalHeadTags); err != nil { + fmt.Fprintf(os.Stderr, " WARN Could not restore: %v\n", err) + } else { + fmt.Fprintf(os.Stderr, " INFO Restored settings for %d screen(s)\n", len(originalHeadTags)) + } + } + + // Stop background processes + if buildWatchCmd != nil && buildWatchCmd.Process != nil { + fmt.Fprintln(os.Stderr, "Stopping build watcher...") + if err := buildWatchCmd.Process.Kill(); err != nil { + fmt.Fprintf(os.Stderr, " Error stopping build watcher: %v\n", err) + } + } + + if serveCmd != nil && serveCmd.Process != nil && serveStarted { + fmt.Fprintln(os.Stderr, "Stopping local server...") + if err := serveCmd.Process.Kill(); err != nil { + fmt.Fprintf(os.Stderr, " Error stopping server: %v\n", err) + } + } + + fmt.Fprintln(os.Stderr, "\nACUL connected mode stopped. Goodbye!") + + watcher.Close() + return nil case <-ctx.Done(): return ctx.Err() @@ -547,14 +650,14 @@ func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, scre headTags, err := buildHeadTagsFromDirs(distPath, assetsURL, screen) if err != nil { if cli.debug { - fmt.Println("⚠️ " + ansi.Yellow(fmt.Sprintf("Skipping '%s': %v", screen, err))) + cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf("Skipping '%s': %v", screen, err))) } continue } if reflect.DeepEqual(lastHeadTags[screen], headTags) { if cli.debug { - fmt.Println("🔁 " + ansi.Cyan(fmt.Sprintf("No changes detected for '%s'", screen))) + cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf("No changes detected for '%s'", screen))) } continue } @@ -574,7 +677,7 @@ func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, scre if len(renderings) == 0 { if cli.debug { - cli.renderer.Infof(ansi.Cyan("🔁 No screens to patch")) + cli.renderer.Warnf(ansi.Cyan("No screens to patch")) } return nil } @@ -583,7 +686,8 @@ func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, scre if err := cli.api.Prompt.BulkUpdateRendering(ctx, req); err != nil { return fmt.Errorf("bulk patch error: %w", err) } - fmt.Println(ansi.Green(fmt.Sprintf("✅ Patched %d screen(s): %s", len(updated), strings.Join(updated, ", ")))) + + cli.renderer.Infof(ansi.Green(fmt.Sprintf("✅ Patched %d screen(s): %s", len(updated), strings.Join(updated, ", ")))) return nil } From ff68db2cc0b98d9e42f0c578dceb2ddd227e5ab4 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Thu, 6 Nov 2025 20:54:53 +0530 Subject: [PATCH 47/58] enhance ACUL scaffolding with improved error handling and npm install functionality --- internal/cli/acul_app_scaffolding.go | 72 +++++++++++++++++++++------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index 6c14e6bc2..4dc29fdcf 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -50,20 +50,20 @@ type Metadata struct { Description string `json:"description"` } -// loadManifest loads manifest.json once. +// loadManifest downloads and parses the manifest.json for the latest release. func loadManifest() (*Manifest, error) { latestTag, err := getLatestReleaseTag() if err != nil { return nil, fmt.Errorf("failed to get latest release tag: %w", err) } + client := &http.Client{Timeout: 15 * time.Second} url := fmt.Sprintf("https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/%s/manifest.json", latestTag) - resp, err := http.Get(url) + resp, err := client.Get(url) if err != nil { return nil, fmt.Errorf("cannot fetch manifest: %w", err) } - defer resp.Body.Close() if resp.StatusCode != http.StatusOK { @@ -85,9 +85,10 @@ func loadManifest() (*Manifest, error) { // getLatestReleaseTag fetches the latest tag from GitHub API. func getLatestReleaseTag() (string, error) { + client := &http.Client{Timeout: 15 * time.Second} url := "https://api.github.com/repos/auth0-samples/auth0-acul-samples/tags" - resp, err := http.Get(url) + resp, err := client.Get(url) if err != nil { return "", fmt.Errorf("failed to fetch tags: %w", err) } @@ -200,11 +201,12 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string, inputs *struct { } tempUnzipDir, err := downloadAndUnzipSampleRepo() - defer os.RemoveAll(tempUnzipDir) // Clean up the entire temp directory. if err != nil { return err } + defer os.RemoveAll(tempUnzipDir) + selectedTemplate := manifest.Templates[chosenTemplate] err = copyTemplateBaseDirs(cli, selectedTemplate.BaseDirectories, chosenTemplate, tempUnzipDir, destDir) @@ -227,6 +229,10 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string, inputs *struct { fmt.Printf("Failed to write config: %v\n", err) } + if prompt.Confirm("Do you want to run npm install?") { + runNpmInstall(cli, destDir) + } + runNpmGenerateScreenLoader(cli, destDir) showPostScaffoldingOutput(cli, destDir, "Project successfully created") @@ -259,6 +265,7 @@ func selectTemplate(cmd *cobra.Command, manifest *Manifest, providedTemplate str if err != nil { return "", handleInputError(err) } + return nameToKey[chosenTemplateName], nil } @@ -339,7 +346,7 @@ func downloadAndUnzipSampleRepo() (string, error) { // TODO: repoURL := fmt.Sprintf("https://github.com/auth0-samples/auth0-acul-samples/archive/refs/tags/%s.zip", latestTag). repoURL := "https://github.com/auth0-samples/auth0-acul-samples/archive/refs/heads/monorepo-sample.zip" - tempZipFile := downloadFile(repoURL) + tempZipFile, err := downloadFile(repoURL) defer os.Remove(tempZipFile) // Clean up the temp zip file. tempUnzipDir, err := os.MkdirTemp("", "unzipped-repo-*") @@ -434,7 +441,7 @@ func copyProjectScreens(cli *cli, screens []Screens, selectedScreens []string, c return fmt.Errorf("failed to find extracted directory: %w", err) } - sourcePathPrefix := extractedDir + "/" + chosenTemplate + sourcePathPrefix := filepath.Join(extractedDir, chosenTemplate) screenInfo := createScreenMap(screens) for _, s := range selectedScreens { screen := screenInfo[s] @@ -493,23 +500,30 @@ func check(err error, msg string) { } // downloadFile downloads a file from a URL to a temporary file and returns its name. -func downloadFile(url string) string { - tempFile, err := os.CreateTemp("", "github-zip-*.zip") - check(err, "Error creating temporary file") +func downloadFile(url string) (string, error) { + client := &http.Client{Timeout: 15 * time.Second} - resp, err := http.Get(url) - check(err, "Error downloading file") + resp, err := client.Get(url) + if err != nil { + return "", fmt.Errorf("failed to download %s: %w", url, err) + } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - log.Printf("Bad status code: %s", resp.Status) + return "", fmt.Errorf("unexpected status code %d when downloading %s", resp.StatusCode, url) + } + + tempFile, err := os.CreateTemp("", "github-zip-*.zip") + if err != nil { + return "", fmt.Errorf("failed to create temp file: %w", err) } + defer tempFile.Close() - _, err = io.Copy(tempFile, resp.Body) - check(err, "Error saving zip file") - tempFile.Close() + if _, err := io.Copy(tempFile, resp.Body); err != nil { + return "", fmt.Errorf("failed to save zip file: %w", err) + } - return tempFile.Name() + return tempFile.Name(), nil } // Function to copy a file from a source path to a destination path. @@ -690,3 +704,27 @@ func runNpmGenerateScreenLoader(cli *cli, destDir string) { return } } + +// runNpmInstall runs `npm install` in the given directory. +// Prints concise logs; warns on failure, silent if successful. +func runNpmInstall(cli *cli, destDir string) { + cmd := exec.Command("npm", "install") + cmd.Dir = destDir + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + cli.renderer.Warnf( + "⚠️ npm install failed: %v\n"+ + "👉 Run manually: %s\n"+ + "📦 Directory: %s\n"+ + "💡 Tip: Check your Node.js and npm setup, or clear node_modules and retry.", + err, + ansi.Bold(ansi.Cyan(fmt.Sprintf("cd %s && npm install", destDir))), + ansi.Faint(destDir), + ) + } + + fmt.Println("✅ " + ansi.Green("All dependencies installed successfully")) +} From ea19a94d8bb583fc696b2ea7557779e492cb2355 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Fri, 7 Nov 2025 09:20:05 +0530 Subject: [PATCH 48/58] improve ACUL scaffolding with handling,prerequisite checks and screen validation --- internal/cli/acul_app_scaffolding.go | 79 +++++++++++++--------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index 4dc29fdcf..a436834e3 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "os" "os/exec" @@ -51,14 +50,9 @@ type Metadata struct { } // loadManifest downloads and parses the manifest.json for the latest release. -func loadManifest() (*Manifest, error) { - latestTag, err := getLatestReleaseTag() - if err != nil { - return nil, fmt.Errorf("failed to get latest release tag: %w", err) - } - +func loadManifest(tag string) (*Manifest, error) { client := &http.Client{Timeout: 15 * time.Second} - url := fmt.Sprintf("https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/%s/manifest.json", latestTag) + url := fmt.Sprintf("https://raw.githubusercontent.com/auth0-samples/auth0-acul-samples/%s/manifest.json", tag) resp, err := client.Get(url) if err != nil { @@ -156,6 +150,12 @@ The generated project includes all necessary configuration and boilerplate code auth0 acul init acul-sample-app --template react --screens login,signup auth0 acul init acul-sample-app -t react -s login,mfa,signup`, RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + if err := ensureACULPrerequisites(ctx, cli.api); err != nil { + return err + } + return runScaffold(cli, cmd, args, &inputs) }, } @@ -179,7 +179,7 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string, inputs *struct { return fmt.Errorf("failed to get latest release tag: %w", err) } - manifest, err := loadManifest() + manifest, err := loadManifest(latestTag) if err != nil { return err } @@ -189,7 +189,7 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string, inputs *struct { return err } - selectedScreens, err := selectScreens(cli, manifest.Templates[chosenTemplate].Screens, inputs.Screens) + selectedScreens, err := validateAndSelectScreens(cli, manifest.Templates[chosenTemplate].Screens, inputs.Screens) if err != nil { return err } @@ -229,12 +229,12 @@ func runScaffold(cli *cli, cmd *cobra.Command, args []string, inputs *struct { fmt.Printf("Failed to write config: %v\n", err) } + runNpmGenerateScreenLoader(cli, destDir) + if prompt.Confirm("Do you want to run npm install?") { runNpmInstall(cli, destDir) } - runNpmGenerateScreenLoader(cli, destDir) - showPostScaffoldingOutput(cli, destDir, "Project successfully created") return nil @@ -269,19 +269,17 @@ func selectTemplate(cmd *cobra.Command, manifest *Manifest, providedTemplate str return nameToKey[chosenTemplateName], nil } -func selectScreens(cli *cli, screens []Screens, providedScreens []string) ([]string, error) { +func validateAndSelectScreens(cli *cli, screens []Screens, providedScreens []string) ([]string, error) { var availableScreenIDs []string for _, s := range screens { availableScreenIDs = append(availableScreenIDs, s.ID) } - // If screens provided via flag, validate them. if len(providedScreens) > 0 { var validScreens []string var invalidScreens []string for _, providedScreen := range providedScreens { - // Skip empty strings. if strings.TrimSpace(providedScreen) == "" { continue } @@ -300,15 +298,12 @@ func selectScreens(cli *cli, screens []Screens, providedScreens []string) ([]str } if len(invalidScreens) > 0 { - cli.renderer.Warnf("%s The following screens are not supported for the chosen template: %s", - ansi.Bold(ansi.Yellow("⚠️")), + cli.renderer.Warnf("⚠️ The following screens are not supported for the chosen template: %s", ansi.Bold(ansi.Red(strings.Join(invalidScreens, ", ")))) - cli.renderer.Infof("%s %s", - ansi.Bold("Available screens:"), + cli.renderer.Infof("Available screens: %s", ansi.Bold(ansi.Cyan(strings.Join(availableScreenIDs, ", ")))) - cli.renderer.Infof("%s %s", - ansi.Bold(ansi.Blue("Note:")), - ansi.Faint("We're planning to support all screens in the future.")) + cli.renderer.Infof("%s We're planning to support all screens in the future.", + ansi.Blue("Note:")) } if len(validScreens) == 0 { @@ -347,6 +342,10 @@ func downloadAndUnzipSampleRepo() (string, error) { // TODO: repoURL := fmt.Sprintf("https://github.com/auth0-samples/auth0-acul-samples/archive/refs/tags/%s.zip", latestTag). repoURL := "https://github.com/auth0-samples/auth0-acul-samples/archive/refs/heads/monorepo-sample.zip" tempZipFile, err := downloadFile(repoURL) + if err != nil { + return "", fmt.Errorf("error downloading sample repo: %w", err) + } + defer os.Remove(tempZipFile) // Clean up the temp zip file. tempUnzipDir, err := os.MkdirTemp("", "unzipped-repo-*") @@ -474,7 +473,8 @@ func writeAculConfig(destDir, chosenTemplate string, selectedScreens []string, m config := AculConfig{ ChosenTemplate: chosenTemplate, Screens: selectedScreens, - InitTimestamp: time.Now().Format(time.RFC3339), + CreatedAt: time.Now().UTC().Format(time.RFC3339), + ModifiedAt: time.Now().UTC().Format(time.RFC3339), AculManifestVersion: manifestVersion, AppVersion: appVersion, } @@ -492,13 +492,6 @@ func writeAculConfig(destDir, chosenTemplate string, selectedScreens []string, m return nil } -// Helper function to handle errors and log them, exiting the process. -func check(err error, msg string) { - if err != nil { - log.Fatalf("%s: %v", msg, err) - } -} - // downloadFile downloads a file from a URL to a temporary file and returns its name. func downloadFile(url string) (string, error) { client := &http.Client{Timeout: 15 * time.Second} @@ -603,21 +596,24 @@ func showPostScaffoldingOutput(cli *cli, destDir, successMessage string) { // Show next steps and related commands. cli.renderer.Infof("%s Next Steps: Navigate to %s and run:", ansi.Bold("🚀"), ansi.Bold(ansi.Cyan(destDir))) - cli.renderer.Infof(" 1. %s", ansi.Bold(ansi.Cyan("npm install"))) - cli.renderer.Infof(" 2. %s", ansi.Bold(ansi.Cyan("npm run build"))) - cli.renderer.Infof(" 3. %s", ansi.Bold(ansi.Cyan("npm run screen dev"))) + cli.renderer.Infof(" %s if not yet installed", ansi.Bold(ansi.Cyan("npm install"))) + cli.renderer.Infof(" %s", ansi.Bold(ansi.Cyan("auth0 acul dev"))) cli.renderer.Output("") fmt.Printf("%s Available Commands:\n", ansi.Bold("📋")) - fmt.Printf(" %s - Add more screens to your project\n", + fmt.Printf(" %s - Add authentication screens\n", ansi.Bold(ansi.Green("auth0 acul screen add "))) - fmt.Printf(" %s - Generate a stub config file\n", + fmt.Printf(" %s - Local development with hot-reload\n", + ansi.Bold(ansi.Green("auth0 acul dev"))) + fmt.Printf(" %s - Live sync changes to Auth0 tenant\n", + ansi.Bold(ansi.Green("auth0 acul dev --connected"))) + fmt.Printf(" %s - Create starter config template\n", ansi.Bold(ansi.Green("auth0 acul config generate "))) - fmt.Printf(" %s - Download current settings\n", + fmt.Printf(" %s - Pull current Auth0 settings\n", ansi.Bold(ansi.Green("auth0 acul config get "))) - fmt.Printf(" %s - Upload customizations\n", + fmt.Printf(" %s - Push local config to Auth0\n", ansi.Bold(ansi.Green("auth0 acul config set "))) - fmt.Printf(" %s - View available screens\n", + fmt.Printf(" %s - List all configurable screens\n", ansi.Bold(ansi.Green("auth0 acul config list"))) fmt.Println() @@ -628,8 +624,9 @@ func showPostScaffoldingOutput(cli *cli, destDir, successMessage string) { type AculConfig struct { ChosenTemplate string `json:"chosen_template"` Screens []string `json:"screens"` - InitTimestamp string `json:"init_timestamp"` - AppVersion string `json:"app_version,omitempty"` + CreatedAt string `json:"created_at"` + ModifiedAt string `json:"modified_at"` + AppVersion string `json:"app_version"` AculManifestVersion string `json:"acul_manifest_version"` } @@ -647,7 +644,7 @@ func checkNodeVersion(cli *cli) { cmd := exec.Command("node", "--version") output, err := cmd.Output() if err != nil { - cli.renderer.Warnf("Unable to detect Node version. Please ensure Node v22+ is installed.") + cli.renderer.Warnf(ansi.Yellow("Unable to detect Node version. Please ensure Node v22+ is installed.")) return } From 6ac2d364bdb0b30d681fed199fd6a7206df86dbb Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Fri, 7 Nov 2025 17:36:44 +0530 Subject: [PATCH 49/58] enhance ACUL command with prerequisite checks and connected mode validation --- internal/cli/acul_dev_connected.go | 58 +++++++++++++++--------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/internal/cli/acul_dev_connected.go b/internal/cli/acul_dev_connected.go index 67fc0464d..24160df7b 100644 --- a/internal/cli/acul_dev_connected.go +++ b/internal/cli/acul_dev_connected.go @@ -93,6 +93,10 @@ CONNECTED MODE (--connected): auth0 acul dev --connected --screen login-id auth0 acul dev -c -s login-id,signup`, RunE: func(cmd *cobra.Command, args []string) error { + if err := ensureACULPrerequisites(cmd.Context(), cli.api); err != nil { + return err + } + pwd, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get current directory: %v", err) @@ -109,7 +113,31 @@ CONNECTED MODE (--connected): return fmt.Errorf("invalid ACUL project: %w", err) } - return runAculDev(cmd, cli, projectDir, port, screenDirs, connected) + if connected { + if confirmed := showConnectedModeInformation(); !confirmed { + fmt.Println(ansi.Red("❌ Connected mode cancelled.")) + return nil + } + + fmt.Println("") + fmt.Println("⚠️ " + ansi.Bold(ansi.Yellow("🌟 CONNECTED MODE ENABLED 🌟"))) + fmt.Println("") + + screensToWatch, err := selectScreensSimple(cli, projectDir, screenDirs) + if err != nil { + return fmt.Errorf("failed to determine screens to watch: %w", err) + } + + return runConnectedMode(cmd.Context(), cli, projectDir, port, screensToWatch) + } + + if port == "" { + err := portFlag.Ask(cmd, &projectDir, auth0.String("8080")) + if err != nil { + return err + } + } + return runNormalMode(cli, projectDir, screenDirs) }, } @@ -121,34 +149,6 @@ CONNECTED MODE (--connected): return cmd } -func runAculDev(cmd *cobra.Command, cli *cli, projectDir, port string, screenDirs []string, connected bool) error { - if connected { - if confirmed := showConnectedModeInformation(); !confirmed { - fmt.Println(ansi.Red("❌ Connected mode cancelled.")) - return nil - } - - fmt.Println("") - fmt.Println("⚠️ " + ansi.Bold(ansi.Yellow("🌟 CONNECTED MODE ENABLED 🌟"))) - fmt.Println("") - - screensToWatch, err := selectScreensSimple(cli, projectDir, screenDirs) - if err != nil { - return fmt.Errorf("failed to determine screens to watch: %w", err) - } - - return runConnectedMode(cmd.Context(), cli, projectDir, port, screensToWatch) - } - - if port == "" { - err := portFlag.Ask(cmd, &projectDir, auth0.String("8080")) - if err != nil { - return err - } - } - return runNormalMode(cli, projectDir, screenDirs) -} - // ToDo : use the port logic. func runNormalMode(cli *cli, projectDir string, screenDirs []string) error { var screen string From 9199da59f614fbcc4a9453a1d0b6b426c69c6893 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Tue, 11 Nov 2025 23:43:14 +0530 Subject: [PATCH 50/58] refactor screen validation and selection logic --- internal/cli/acul_app_scaffolding.go | 105 +++++++------ internal/cli/acul_dev_connected.go | 225 ++++++++++++++------------- 2 files changed, 168 insertions(+), 162 deletions(-) diff --git a/internal/cli/acul_app_scaffolding.go b/internal/cli/acul_app_scaffolding.go index 33478ccce..954eb6dd9 100644 --- a/internal/cli/acul_app_scaffolding.go +++ b/internal/cli/acul_app_scaffolding.go @@ -9,6 +9,7 @@ import ( "os/exec" "path/filepath" "regexp" + "slices" "strconv" "strings" "time" @@ -272,57 +273,47 @@ func selectTemplate(cmd *cobra.Command, manifest *Manifest, providedTemplate str return nameToKey[chosenTemplateName], nil } -// validateAndSelectScreens is a common function for screen validation and selection. +// validateAndSelectScreens validates provided screens or prompts for selection. func validateAndSelectScreens(cli *cli, screenIDs []string, providedScreens []string) ([]string, error) { - if len(providedScreens) > 0 { - var validScreens []string - var invalidScreens []string + if len(screenIDs) == 0 { + return nil, fmt.Errorf("no available screens found") + } - for _, providedScreen := range providedScreens { - if strings.TrimSpace(providedScreen) == "" { + if len(providedScreens) > 0 { + var valid, invalid []string + for _, s := range providedScreens { + trimmedScreen := strings.TrimSpace(s) + if trimmedScreen == "" { continue } - - found := false - for _, availableScreen := range screenIDs { - if providedScreen == availableScreen { - validScreens = append(validScreens, providedScreen) - found = true - break - } - } - if !found { - invalidScreens = append(invalidScreens, providedScreen) + if slices.Contains(screenIDs, trimmedScreen) { + valid = append(valid, trimmedScreen) + } else { + invalid = append(invalid, trimmedScreen) } } - if len(invalidScreens) > 0 { - cli.renderer.Warnf("⚠️ The following screens are not supported for the chosen template: %s", - ansi.Bold(ansi.Red(strings.Join(invalidScreens, ", ")))) - cli.renderer.Infof("Available screens: %s", - ansi.Bold(ansi.Cyan(strings.Join(screenIDs, ", ")))) - cli.renderer.Infof("%s We're planning to support all screens in the future.", - ansi.Blue("Note:")) + if len(invalid) > 0 { + cli.renderer.Warnf("Unsupported screens: %s", ansi.Red(strings.Join(invalid, ", "))) + cli.renderer.Infof("Available: %s", ansi.Cyan(strings.Join(screenIDs, ", "))) } - if len(validScreens) == 0 { - cli.renderer.Warnf("%s %s", - ansi.Bold(ansi.Yellow("⚠️")), - ansi.Bold("None of the provided screens are valid for this template.")) - } else { - return validScreens, nil + if len(valid) > 0 { + return valid, nil } - } - // If no screens provided or no valid screens, prompt for multi-select. - var selectedScreens []string - err := prompt.AskMultiSelect("Select screens to include:", &selectedScreens, screenIDs...) + cli.renderer.Warnf("No valid screens found.") + } - if len(selectedScreens) == 0 { + var selected []string + if err := prompt.AskMultiSelect("Select screens:", &selected, screenIDs...); err != nil { + return nil, err + } + if len(selected) == 0 { return nil, fmt.Errorf("at least one screen must be selected") } - return selectedScreens, err + return selected, nil } func getDestDir(args []string) string { @@ -584,40 +575,48 @@ func createScreenMap(screens []Screens) map[string]Screens { func showPostScaffoldingOutput(cli *cli, destDir, successMessage string) { cli.renderer.Output("") cli.renderer.Infof("%s %s in %s!", - ansi.Bold(ansi.Green("🎉")), successMessage, ansi.Bold(ansi.Cyan(fmt.Sprintf("'%s'", destDir)))) + "🎉", successMessage, ansi.Bold(ansi.Cyan(fmt.Sprintf("'%s'", destDir)))) cli.renderer.Output("") cli.renderer.Infof("📖 Explore the sample app: %s", ansi.Blue("https://github.com/auth0-samples/auth0-acul-samples")) cli.renderer.Output("") + // Show next steps and related commands. + fmt.Println() + fmt.Println(ansi.Bold("Next Steps:")) + fmt.Printf(" Navigate to %s\n", ansi.Cyan(destDir)) + fmt.Printf(" Run %s if dependencies are not installed\n", ansi.Cyan("npm install")) + fmt.Printf(" Start the local dev server using %s\n", ansi.Cyan("auth0 acul dev")) + + printAvailableCommands() checkNodeVersion(cli) +} - // Show next steps and related commands. - cli.renderer.Infof("%s Next Steps: Navigate to %s and run:", ansi.Bold("🚀"), ansi.Bold(ansi.Cyan(destDir))) - cli.renderer.Infof(" %s if not yet installed", ansi.Bold(ansi.Cyan("npm install"))) - cli.renderer.Infof(" %s", ansi.Bold(ansi.Cyan("auth0 acul dev"))) - cli.renderer.Output("") +func printAvailableCommands() { + fmt.Println() + fmt.Println(ansi.Bold("📋 Available Commands:")) + fmt.Println() - fmt.Printf("%s Available Commands:\n", ansi.Bold("📋")) - fmt.Printf(" %s - Add authentication screens\n", + fmt.Printf(" %s - Add authentication screens\n", ansi.Bold(ansi.Green("auth0 acul screen add "))) - fmt.Printf(" %s - Local development with hot-reload\n", + fmt.Printf(" %s - Local development with hot-reload\n", ansi.Bold(ansi.Green("auth0 acul dev"))) - fmt.Printf(" %s - Live sync changes to Auth0 tenant\n", + fmt.Printf(" %s - Live sync changes to Auth0 tenant\n", ansi.Bold(ansi.Green("auth0 acul dev --connected"))) - fmt.Printf(" %s - Create starter config template\n", + fmt.Printf(" %s - Create starter config template\n", ansi.Bold(ansi.Green("auth0 acul config generate "))) - fmt.Printf(" %s - Pull current Auth0 settings\n", + fmt.Printf(" %s - Pull current Auth0 settings\n", ansi.Bold(ansi.Green("auth0 acul config get "))) - fmt.Printf(" %s - Push local config to Auth0\n", + fmt.Printf(" %s - Push local config to Auth0\n", ansi.Bold(ansi.Green("auth0 acul config set "))) - fmt.Printf(" %s - List all configurable screens\n", + fmt.Printf(" %s - List all configurable screens\n", ansi.Bold(ansi.Green("auth0 acul config list"))) - fmt.Println() - fmt.Printf("%s %s: Use %s to see all available commands\n", - ansi.Bold("💡"), ansi.Bold("Tip"), ansi.Bold(ansi.Cyan("'auth0 acul --help'"))) + fmt.Println() + fmt.Printf("%s Use %s to see all available commands\n", + ansi.Yellow("💡 Tip:"), ansi.Cyan("auth0 acul --help")) + fmt.Println() } type AculConfig struct { diff --git a/internal/cli/acul_dev_connected.go b/internal/cli/acul_dev_connected.go index 24160df7b..8a8cdc44f 100644 --- a/internal/cli/acul_dev_connected.go +++ b/internal/cli/acul_dev_connected.go @@ -8,6 +8,7 @@ import ( "os/signal" "path/filepath" "reflect" + "strconv" "strings" "syscall" "time" @@ -37,6 +38,7 @@ var ( IsRequired: false, AlwaysPrompt: false, } + portFlag = Flag{ Name: "Port", LongForm: "port", @@ -44,6 +46,7 @@ var ( Help: "Port for the local development server.", IsRequired: false, } + connectedFlag = Flag{ Name: "Connected", LongForm: "connected", @@ -190,19 +193,20 @@ func showConnectedModeInformation() bool { fmt.Println("📢 " + ansi.Bold(ansi.Cyan("Connected Mode Information"))) fmt.Println("") fmt.Println("ℹ️ " + ansi.Cyan("This mode updates advanced rendering settings for selected screens in your Auth0 tenant.")) + fmt.Println("") fmt.Println("🚨 " + ansi.Bold(ansi.Red("IMPORTANT: Never use on production tenants!"))) - fmt.Println(" " + ansi.Yellow("Production may break sessions or incur unexpected charges with local assets.")) - fmt.Println(" " + ansi.Yellow("Use ONLY for dev/stage tenants.")) + fmt.Println(" " + ansi.Yellow("• Production may break sessions or incur unexpected charges with local assets.")) + fmt.Println(" " + ansi.Yellow("• Use ONLY for dev/stage tenants.")) fmt.Println("") fmt.Println("⚙️ " + ansi.Bold(ansi.Magenta("Technical Requirements:"))) fmt.Println(" " + ansi.Cyan("• Requires sample apps with viteConfig.ts configured for asset building")) fmt.Println(" " + ansi.Cyan("• Assets must be built in the following structure:")) - fmt.Println(" " + ansi.Green("assets//")) - fmt.Println(" " + ansi.Green("assets//")) - fmt.Println(" " + ansi.Green("assets/")) + fmt.Println(" " + ansi.Green(" assets//")) + fmt.Println(" " + ansi.Green(" assets//")) + fmt.Println(" " + ansi.Green(" assets/")) fmt.Println("") - fmt.Println("🔄 " + ansi.Bold(ansi.Magenta("How it works:"))) + fmt.Println("🔄 " + ansi.Bold(ansi.Magenta("How it works:"))) fmt.Println(" " + ansi.Cyan("• Combines files from screen-specific, shared, and main asset folders")) fmt.Println(" " + ansi.Cyan("• Makes API patch calls to update rendering settings for each specified screen")) fmt.Println(" " + ansi.Cyan("• Watches for changes and automatically re-patches when assets are rebuilt")) @@ -212,8 +216,9 @@ func showConnectedModeInformation() bool { } func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, screensToWatch []string) error { - fmt.Println("🚀 " + ansi.Green(fmt.Sprintf("ACUL connected dev mode started for %s", projectDir))) + fmt.Println("\n🚀 " + ansi.Green("ACUL connected dev mode started for: "+ansi.Cyan(projectDir))) + // Step 1: Do initial build. fmt.Println("") fmt.Println("🔨 " + ansi.Bold(ansi.Blue("Step 1: Running initial build..."))) if err := buildProject(cli, projectDir); err != nil { @@ -226,36 +231,34 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc return fmt.Errorf("screen validation failed after build: %w", err) } + // Step 2: Ask user to host assets and get port confirmation. fmt.Println("") fmt.Println("📡 " + ansi.Bold(ansi.Blue("Step 2: Host your assets locally"))) if port == "" { var portInput string - portQuestion := prompt.TextInput( - "port", - "Enter the port for serving assets:", - "The port number where your assets will be hosted (e.g., 8080)", - "8080", - true, - ) + portQuestion := prompt.TextInput("port", "Enter port to serve assets:", "Example: 8080", "8080", true) if err := prompt.AskOne(portQuestion, &portInput); err != nil { return fmt.Errorf("failed to get port: %w", err) } + + if _, err = strconv.Atoi(portInput); err != nil { + return fmt.Errorf("invalid port number: %s", portInput) + } + port = portInput } - fmt.Println("💡 " + ansi.Yellow("Your assets need to be served locally with CORS enabled.")) + fmt.Println("💡 " + ansi.Yellow("Your assets must be served locally with CORS enabled.")) runServe := prompt.Confirm(fmt.Sprintf("Would you like to host the assets by running 'npx serve dist -p %s --cors' in the background?", port)) - var ( - serveCmd *exec.Cmd - serveStarted bool - ) + var serveStarted bool + if runServe { fmt.Println("🚀 " + ansi.Cyan("Starting local server in the background...")) - serveCmd = exec.Command("npx", "serve", "dist", "-p", port, "--cors") + serveCmd := exec.Command("npx", "serve", "dist", "-p", port, "--cors") serveCmd.Dir = projectDir if cli.debug { @@ -273,10 +276,10 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc time.Sleep(2 * time.Second) // Give server time to start. } } else { - fmt.Println("📋 " + ansi.Cyan("Please host your assets manually using:")) + fmt.Println("Please either run the following command in a separate terminal to serve your assets or host someway on your own") fmt.Println(" " + ansi.Bold(ansi.Green(fmt.Sprintf("npx serve dist -p %s --cors", port)))) fmt.Println("") - fmt.Println("💡 " + ansi.Yellow("This will serve your built assets with CORS enabled.")) + fmt.Println("This will serve your built assets at the specified port with CORS enabled.") } assetsURL := fmt.Sprintf("http://localhost:%s", port) @@ -290,19 +293,19 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc } } + // Step 3: Ask about build:watch. fmt.Println("") fmt.Println("🔧 " + ansi.Bold(ansi.Blue("Step 3: Continuous build watching (optional)"))) fmt.Println(" " + ansi.Green("1. Manually run 'npm run build' after changes, OR")) fmt.Println(" " + ansi.Green("2. Run 'npm run build:watch' for continuous updates")) fmt.Println("") - fmt.Println("💡 " + ansi.Yellow("Note: If auto-save is enabled in your IDE, build:watch will rebuild frequently.")) + fmt.Println("💡 " + ansi.Yellow("If auto-save is enabled in your IDE, build:watch will rebuild frequently.")) runBuildWatch := prompt.Confirm("Would you like to run 'npm run build:watch' in the background?") - var buildWatchCmd *exec.Cmd if runBuildWatch { fmt.Println("🚀 " + ansi.Cyan("Starting 'npm run build:watch' in the background...")) - buildWatchCmd = exec.Command("npm", "run", "build:watch") + buildWatchCmd := exec.Command("npm", "run", "build:watch") buildWatchCmd.Dir = projectDir // Only show command output if debug mode is enabled. @@ -320,7 +323,7 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc } fmt.Println("") - fmt.Println("👀 " + ansi.Bold(ansi.Blue("Step 4: Starting asset watcher and patching..."))) + fmt.Println("👀 " + ansi.Bold(ansi.Blue("Step 4: Start watching assets and auto-patching..."))) distPath := filepath.Join(projectDir, "dist") @@ -328,23 +331,24 @@ func runConnectedMode(ctx context.Context, cli *cli, projectDir, port string, sc fmt.Println("👀 Watching screens: " + ansi.Cyan(strings.Join(screensToWatch, ", "))) // Fetch original head tags before starting watcher. - fmt.Println("💡 " + ansi.Cyan("Fetching original rendering settings for restoration on exit...")) + fmt.Println("💡 " + ansi.Yellow("Note: Original rendering settings will be automatically restored on exit.")) originalHeadTags, err := fetchOriginalHeadTags(ctx, cli, screensToWatch) if err != nil { - fmt.Println("⚠️ " + ansi.Yellow(fmt.Sprintf("Warning: Could not fetch original settings: %v", err))) - fmt.Println(" " + ansi.Yellow("Original settings will not be restored on exit.")) + fmt.Println("⚠️ " + ansi.Yellow(fmt.Sprintf("Could not fetch original settings: %v", err))) + fmt.Println(" " + ansi.Yellow("Restoration will be skipped since no previous settings could be retrieved.")) originalHeadTags = nil // Continue without restoration capability. } else { - fmt.Println("✅ " + ansi.Green(fmt.Sprintf("Original settings saved for %d screen(s)", len(originalHeadTags)))) + fmt.Println("✅ " + ansi.Green(fmt.Sprintf("Saved original settings for %d screen(s)", len(originalHeadTags)))) } - fmt.Println("") - fmt.Println("💡 " + ansi.Green("Assets will be patched automatically when changes are detected in the dist folder")) - fmt.Println("") - fmt.Println(ansi.Bold(ansi.Magenta("Tip: Run 'auth0 test login' to see your changes in action!"))) - fmt.Println(ansi.Cyan("Press Ctrl+C to stop and restore original settings")) + fmt.Println() + fmt.Println(ansi.Magenta("💡 Tips:")) + fmt.Println(ansi.Cyan(" • Assets in '/dist/assets' are continuously monitored and patched when changes occur.")) + fmt.Println(ansi.Cyan(" • Run 'auth0 test login' anytime to preview your changes in real-time.")) + fmt.Println(ansi.Cyan(" • Press Ctrl+C to stop and restore your previous rendering settings.")) + fmt.Println() - return watchAndPatch(ctx, cli, assetsURL, distPath, screensToWatch, buildWatchCmd, serveCmd, serveStarted, originalHeadTags) + return watchAndPatch(ctx, cli, assetsURL, distPath, screensToWatch, originalHeadTags) } func validateAculProject(projectDir string) error { @@ -391,8 +395,6 @@ func selectScreensSimple(cli *cli, projectDir string, screenDirs []string) ([]st srcScreensPath := filepath.Join(projectDir, "src", "screens") if availableScreens, err := getScreensFromSrcFolder(srcScreensPath); err == nil && len(availableScreens) > 0 { - cli.renderer.Infof(ansi.Cyan(fmt.Sprintf("📂 Detected screens in src/screens: %s", strings.Join(availableScreens, ", ")))) - return validateAndSelectScreens(cli, availableScreens, nil) } @@ -530,10 +532,6 @@ func restoreOriginalHeadTags(ctx context.Context, cli *cli, originalHeadTags map RenderingMode: &management.RenderingModeAdvanced, HeadTags: headTags, }) - - if cli.debug { - fmt.Fprintf(os.Stderr, " 🔄 Restoring '%s'\n", screen) - } } if len(renderings) == 0 { @@ -548,7 +546,7 @@ func restoreOriginalHeadTags(ctx context.Context, cli *cli, originalHeadTags map return nil } -func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screensToWatch []string, buildWatchCmd, serveCmd *exec.Cmd, serveStarted bool, originalHeadTags map[string][]interface{}) error { +func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, screensToWatch []string, originalHeadTags map[string][]interface{}) error { watcher, err := fsnotify.NewWatcher() if err != nil { return fmt.Errorf("failed to create watcher: %w", err) @@ -556,21 +554,36 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc defer watcher.Close() if err := watcher.Add(distPath); err != nil { - cli.renderer.Warnf("Failed to watch %s: %v", distPath, err) + return fmt.Errorf("failed to watch %s: %w", distPath, err) } - cli.renderer.Infof("Watching: %s", strings.Join(screensToWatch, ", ")) + fmt.Println("👀 Watching: " + ansi.Yellow(strings.Join(screensToWatch, ", "))) // First, stop any existing global signal handlers (from root.go). signal.Reset(os.Interrupt, syscall.SIGTERM) sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - defer signal.Stop(sigChan) // Clean up signal handler when function exits. + defer signal.Stop(sigChan) const debounceWindow = 5 * time.Second - var lastEventTime time.Time - lastHeadTags := make(map[string][]interface{}) + var ( + lastEventTime time.Time + lastHeadTags = make(map[string][]interface{}) + ) + + cleanup := func() { + fmt.Fprintln(os.Stderr, "\nShutting down...") + if len(originalHeadTags) > 0 { + fmt.Fprintln(os.Stderr, "Restoring original rendering settings...") + if err := restoreOriginalHeadTags(ctx, cli, originalHeadTags); err != nil { + fmt.Fprintf(os.Stderr, "Could not restore previous settings: %v\n", err) + } else { + fmt.Fprintf(os.Stderr, "Successfully restored rendering settings for %d screen(s).\n", len(originalHeadTags)) + } + } + fmt.Fprintln(os.Stderr, "👋 ACUL connected mode stopped.") + } for { select { @@ -579,8 +592,8 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc return nil } - // Trigger only on changes inside dist/assets/. - if !strings.Contains(event.Name, "assets") { + // Ignore non-asset events or irrelevant file ops. + if event.Op&(fsnotify.Write|fsnotify.Create) == 0 || !strings.Contains(event.Name, "assets") { continue } @@ -591,106 +604,100 @@ func watchAndPatch(ctx context.Context, cli *cli, assetsURL, distPath string, sc lastEventTime = now time.Sleep(500 * time.Millisecond) // Let writes settle. - cli.renderer.Warnf(ansi.Cyan("Change detected, patching assets...")) - if err := patchAssets(ctx, cli, distPath, assetsURL, screensToWatch, lastHeadTags); err != nil { - cli.renderer.Errorf("Patch failed: %v", err) - } - case err := <-watcher.Errors: - cli.renderer.Warnf("Watcher error: %v", err) - - case <-sigChan: - fmt.Fprintln(os.Stderr, "\nShutdown signal received, cleaning up...") + newHeadTags := make(map[string][]interface{}) + changedScreens := make([]string, 0) - // Restore original head tags if available. - if len(originalHeadTags) > 0 { - fmt.Fprintln(os.Stderr, "Restoring original settings...") + for _, screen := range screensToWatch { + headTags, err := buildHeadTagsFromDirs(distPath, assetsURL, screen) + if err != nil { + if cli.debug { + cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf("Skipping '%s': %v", screen, err))) + } + continue + } - if err := restoreOriginalHeadTags(ctx, cli, originalHeadTags); err != nil { - fmt.Fprintf(os.Stderr, " WARN Could not restore: %v\n", err) - } else { - fmt.Fprintf(os.Stderr, " INFO Restored settings for %d screen(s)\n", len(originalHeadTags)) + // Compare with last known tags. + if reflect.DeepEqual(lastHeadTags[screen], headTags) { + continue } + + // Only record changed screens. + newHeadTags[screen] = headTags + changedScreens = append(changedScreens, screen) } - // Stop background processes. - if buildWatchCmd != nil && buildWatchCmd.Process != nil { - fmt.Fprintln(os.Stderr, "Stopping build watcher...") - if err := buildWatchCmd.Process.Kill(); err != nil { - fmt.Fprintf(os.Stderr, " Error stopping build watcher: %v\n", err) + if len(changedScreens) == 0 { + if cli.debug { + fmt.Println(ansi.Yellow("No effective asset changes detected and skipping patch.")) } + continue } - if serveCmd != nil && serveCmd.Process != nil && serveStarted { - fmt.Fprintln(os.Stderr, "Stopping local server...") - if err := serveCmd.Process.Kill(); err != nil { - fmt.Fprintf(os.Stderr, " Error stopping server: %v\n", err) + if cli.debug { + fmt.Println(ansi.Cyan(fmt.Sprintf("🔄 Changes detected in %d screen(s): %s", len(changedScreens), + strings.Join(changedScreens, ", ")))) + } else { + fmt.Println(ansi.Cyan("⚙️ Change detected, patching assets...")) + } + + if err = patchChangedScreens(ctx, cli, changedScreens, newHeadTags); err != nil { + cli.renderer.Errorf("Patch failed: %v", err) + } else { + fmt.Println(ansi.Green("✅ Assets patched successfully!")) + for screen, headTags := range newHeadTags { + lastHeadTags[screen] = headTags } } - fmt.Fprintln(os.Stderr, "\nACUL connected mode stopped. Goodbye!") + case err := <-watcher.Errors: + cli.renderer.Warnf("Watcher error: %v", err) - watcher.Close() + case <-sigChan: + cleanup() + printAvailableCommands() return nil case <-ctx.Done(): + cleanup() + printAvailableCommands() return ctx.Err() } } } -func patchAssets(ctx context.Context, cli *cli, distPath, assetsURL string, screensToWatch []string, lastHeadTags map[string][]interface{}) error { - var ( - renderings []*management.PromptRendering - updated []string - ) - - for _, screen := range screensToWatch { - headTags, err := buildHeadTagsFromDirs(distPath, assetsURL, screen) - if err != nil { - if cli.debug { - cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf("Skipping '%s': %v", screen, err))) - } - continue - } - - if reflect.DeepEqual(lastHeadTags[screen], headTags) { - if cli.debug { - cli.renderer.Warnf(ansi.Yellow(fmt.Sprintf("No changes detected for '%s'", screen))) - } - continue - } - lastHeadTags[screen] = headTags +func patchChangedScreens(ctx context.Context, cli *cli, changedScreens []string, headTagsMap map[string][]interface{}) error { + var renderings []*management.PromptRendering + for _, screen := range changedScreens { promptType := management.PromptType(ScreenPromptMap[screen]) screenType := management.ScreenName(screen) - renderings = append(renderings, &management.PromptRendering{ Prompt: &promptType, Screen: &screenType, RenderingMode: &management.RenderingModeAdvanced, - HeadTags: headTags, + HeadTags: headTagsMap[screen], }) - updated = append(updated, screen) } - if len(renderings) == 0 { - if cli.debug { - cli.renderer.Warnf(ansi.Cyan("No screens to patch")) - } - return nil + req := &management.PromptRenderingUpdateRequest{PromptRenderings: renderings} + if cli.debug { + fmt.Println(ansi.Cyan(fmt.Sprintf("Patching %d screen(s): %s", len(changedScreens), strings.Join(changedScreens, ", ")))) } - req := &management.PromptRenderingUpdateRequest{PromptRenderings: renderings} if err := cli.api.Prompt.BulkUpdateRendering(ctx, req); err != nil { return fmt.Errorf("bulk patch error: %w", err) } - cli.renderer.Infof(ansi.Green(fmt.Sprintf("✅ Patched %d screen(s): %s", len(updated), strings.Join(updated, ", ")))) + if cli.debug { + fmt.Println(ansi.Green(fmt.Sprintf("Patched %d screen(s) successfully: %s", + len(changedScreens), strings.Join(changedScreens, ", ")))) + } return nil } +// buildHeadTagsFromDirs collects