|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "os" |
| 7 | + "os/exec" |
| 8 | + "path" |
| 9 | + "path/filepath" |
| 10 | + "strings" |
| 11 | + "time" |
| 12 | + |
| 13 | + "github.com/google/generative-ai-go/genai" |
| 14 | + "github.com/shopware/extension-verifier/internal/tool" |
| 15 | + "github.com/shopware/extension-verifier/internal/twig" |
| 16 | + "github.com/shopware/shopware-cli/extension" |
| 17 | + "github.com/spf13/cobra" |
| 18 | + "google.golang.org/api/option" |
| 19 | +) |
| 20 | + |
| 21 | +var twigUpgradeCommand = &cobra.Command{ |
| 22 | + Use: "twig-upgrade [path] [old-shopware-version] [new-shopware-version]", |
| 23 | + Short: "Experimental upgrade of Twig templates using AI", |
| 24 | + Args: cobra.ExactArgs(3), |
| 25 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 26 | + ext, err := extension.GetExtensionByFolder(args[0]) |
| 27 | + |
| 28 | + if err != nil { |
| 29 | + return err |
| 30 | + } |
| 31 | + |
| 32 | + toolCfg, err := tool.ConvertExtensionToToolConfig(ext) |
| 33 | + |
| 34 | + if err != nil { |
| 35 | + return err |
| 36 | + } |
| 37 | + |
| 38 | + apiKey := os.Getenv("GEMINI_API_KEY") |
| 39 | + |
| 40 | + if apiKey == "" { |
| 41 | + return fmt.Errorf("GEMINI_API_KEY is not set") |
| 42 | + } |
| 43 | + |
| 44 | + client, err := genai.NewClient(cmd.Context(), option.WithAPIKey(apiKey)) |
| 45 | + |
| 46 | + if err != nil { |
| 47 | + return err |
| 48 | + } |
| 49 | + |
| 50 | + for _, sourceDirectory := range toolCfg.SourceDirectories { |
| 51 | + twigFolder := path.Join(sourceDirectory, "Resources", "views", "storefront") |
| 52 | + |
| 53 | + if _, err := os.Stat(twigFolder); os.IsNotExist(err) { |
| 54 | + return nil |
| 55 | + } |
| 56 | + |
| 57 | + oldVersion, err := cloneShopwareStorefront(args[1]) |
| 58 | + |
| 59 | + if err != nil { |
| 60 | + return err |
| 61 | + } |
| 62 | + |
| 63 | + newVersion, err := cloneShopwareStorefront(args[2]) |
| 64 | + |
| 65 | + if err != nil { |
| 66 | + return err |
| 67 | + } |
| 68 | + |
| 69 | + defer func() { |
| 70 | + if err := os.RemoveAll(oldVersion); err != nil { |
| 71 | + fmt.Fprintf(os.Stderr, "Failed to remove old version directory: %v\n", err) |
| 72 | + } |
| 73 | + }() |
| 74 | + defer func() { |
| 75 | + if err := os.RemoveAll(newVersion); err != nil { |
| 76 | + fmt.Fprintf(os.Stderr, "Failed to remove new version directory: %v\n", err) |
| 77 | + } |
| 78 | + }() |
| 79 | + |
| 80 | + _ = filepath.Walk(twigFolder, func(file string, info os.FileInfo, _ error) error { |
| 81 | + if info.IsDir() { |
| 82 | + return nil |
| 83 | + } |
| 84 | + |
| 85 | + if filepath.Ext(file) != ".twig" { |
| 86 | + return nil |
| 87 | + } |
| 88 | + |
| 89 | + content, err := os.ReadFile(file) |
| 90 | + |
| 91 | + if err != nil { |
| 92 | + return err |
| 93 | + } |
| 94 | + |
| 95 | + ast, err := twig.ParseTemplate(string(content)) |
| 96 | + |
| 97 | + if err != nil { |
| 98 | + return err |
| 99 | + } |
| 100 | + |
| 101 | + extends := ast.Extends() |
| 102 | + |
| 103 | + if extends == nil { |
| 104 | + return nil |
| 105 | + } |
| 106 | + |
| 107 | + tpl := extends.Template |
| 108 | + |
| 109 | + if tpl[0] == '@' { |
| 110 | + tplParts := strings.Split(tpl, "/") |
| 111 | + tplParts = tplParts[1:] |
| 112 | + tpl = strings.Join(tplParts, "/") |
| 113 | + } |
| 114 | + |
| 115 | + oldTemplateText, err := os.ReadFile(path.Join(oldVersion, "Resources", "views", tpl)) |
| 116 | + |
| 117 | + if err != nil { |
| 118 | + fmt.Printf("Template %s not found in old version\n", tpl) |
| 119 | + return nil |
| 120 | + } |
| 121 | + |
| 122 | + newTemplateText, err := os.ReadFile(path.Join(newVersion, "Resources", "views", tpl)) |
| 123 | + |
| 124 | + if err != nil { |
| 125 | + fmt.Printf("Template %s not found in new version\n", tpl) |
| 126 | + return nil |
| 127 | + } |
| 128 | + |
| 129 | + var str strings.Builder |
| 130 | + str.WriteString("You are a helper agent to help to upgrade Twig templates. I will give you the old and new template happend in the Software and as third the extended template. Apply the changes happen between old and new template to the extended template.\n") |
| 131 | + str.WriteString("Follow following rules while making adjustments to the extended template:\n") |
| 132 | + str.WriteString("- Do only the necessary changes to the extended template.\n") |
| 133 | + str.WriteString("- Do only modify the content inside the block and dont add new blocks\n") |
| 134 | + str.WriteString("- Please also only output the modified extended template nothing more.\n") |
| 135 | + str.WriteString("- Adjust also HTML elements to be more accessibility friendly.\n") |
| 136 | + str.WriteString("- If in a {% block %} is {{ parent() }}, ignore it and dont modify the content of the block\n") |
| 137 | + str.WriteString("\n") |
| 138 | + str.WriteString("This was the old template:\n") |
| 139 | + str.WriteString("```twig\n") |
| 140 | + str.WriteString(string(oldTemplateText)) |
| 141 | + str.WriteString("\n```\n") |
| 142 | + str.WriteString("and this is the new one:\n") |
| 143 | + str.WriteString("```twig\n") |
| 144 | + str.WriteString(string(newTemplateText)) |
| 145 | + str.WriteString("\n```\n") |
| 146 | + str.WriteString("and this is my template:\n") |
| 147 | + str.WriteString("```twig\n") |
| 148 | + str.WriteString(string(content)) |
| 149 | + str.WriteString("\n```") |
| 150 | + |
| 151 | + resp, err := generateContent(cmd.Context(), client, str.String()) |
| 152 | + |
| 153 | + if err != nil { |
| 154 | + return err |
| 155 | + } |
| 156 | + |
| 157 | + text := string(resp.Candidates[0].Content.Parts[0].(genai.Text)) |
| 158 | + |
| 159 | + start := strings.Index(text, "```twig") |
| 160 | + end := strings.LastIndex(text, "```") |
| 161 | + |
| 162 | + if start == -1 || end == -1 { |
| 163 | + return nil |
| 164 | + } |
| 165 | + |
| 166 | + text = strings.TrimPrefix(text[start+7:end], "\n") |
| 167 | + |
| 168 | + contentStr := string(content) |
| 169 | + if strings.TrimSpace(text) == strings.TrimSpace(contentStr) { |
| 170 | + return nil |
| 171 | + } |
| 172 | + |
| 173 | + return os.WriteFile(file, []byte(text), os.ModePerm) |
| 174 | + }) |
| 175 | + } |
| 176 | + return nil |
| 177 | + }, |
| 178 | +} |
| 179 | + |
| 180 | +func generateContent(ctx context.Context, client *genai.Client, message string) (*genai.GenerateContentResponse, error) { |
| 181 | + resp, err := client.GenerativeModel("gemini-2.0-pro-exp-02-05").GenerateContent(ctx, genai.Text(message)) |
| 182 | + |
| 183 | + if err != nil { |
| 184 | + if strings.Contains(err.Error(), "Resource has been exhausted") { |
| 185 | + fmt.Println("Resource exhausted, waiting 15 seconds before retrying") |
| 186 | + time.Sleep(15 * time.Second) |
| 187 | + |
| 188 | + return generateContent(ctx, client, message) |
| 189 | + } |
| 190 | + } |
| 191 | + |
| 192 | + return resp, err |
| 193 | +} |
| 194 | + |
| 195 | +func cloneShopwareStorefront(version string) (string, error) { |
| 196 | + tempDir, err := os.MkdirTemp(os.TempDir(), "shopware") |
| 197 | + |
| 198 | + if err != nil { |
| 199 | + return "", err |
| 200 | + } |
| 201 | + |
| 202 | + git := exec.Command("git", "clone", "--branch", "v"+version, "https://github.com/shopware/storefront", tempDir, "--depth", "1") |
| 203 | + git.Stdout = os.Stdout |
| 204 | + git.Stderr = os.Stderr |
| 205 | + |
| 206 | + if err := git.Run(); err != nil { |
| 207 | + return "", err |
| 208 | + } |
| 209 | + |
| 210 | + return tempDir, nil |
| 211 | +} |
| 212 | + |
| 213 | +func init() { |
| 214 | + rootCmd.AddCommand(twigUpgradeCommand) |
| 215 | +} |
0 commit comments