From 8ce28b9b465e2815f62be4615970874adde36b71 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Wed, 27 Aug 2025 12:53:12 +0530 Subject: [PATCH 01/29] 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 02/29] 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 03/29] 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 04/29] 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 05/29] 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 06/29] 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 07/29] 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 08/29] 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 09/29] 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 10/29] 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 11/29] 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 12/29] 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 13/29] 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 14/29] 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 15/29] 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 16/29] 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 17/29] 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 18/29] 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 19/29] 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 20/29] 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 21/29] 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 22/29] 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 23/29] 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 24/29] 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 2a5813142f54bb870d36e3415bf75a4e2d833605 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Mon, 3 Nov 2025 23:06:24 +0530 Subject: [PATCH 25/29] 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 26/29] 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 27/29] 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 ff68db2cc0b98d9e42f0c578dceb2ddd227e5ab4 Mon Sep 17 00:00:00 2001 From: ramya18101 Date: Thu, 6 Nov 2025 20:54:53 +0530 Subject: [PATCH 28/29] 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 29/29] 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 }