Skip to content

Commit 93a804f

Browse files
authored
cli: generate llm rules for apps
1 parent 5ec5de5 commit 93a804f

File tree

11 files changed

+974
-305
lines changed

11 files changed

+974
-305
lines changed

cli/cmd/encore/app/create.go

Lines changed: 44 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8-
"io"
98
"io/fs"
10-
"net/http"
119
"os"
1210
"os/exec"
1311
"path/filepath"
@@ -23,52 +21,37 @@ import (
2321

2422
"encr.dev/cli/cmd/encore/auth"
2523
"encr.dev/cli/cmd/encore/cmdutil"
24+
"encr.dev/cli/cmd/encore/llm_rules"
2625
"encr.dev/cli/internal/platform"
2726
"encr.dev/cli/internal/telemetry"
2827
"encr.dev/internal/conf"
2928
"encr.dev/internal/env"
29+
"encr.dev/internal/userconfig"
3030
"encr.dev/internal/version"
3131
"encr.dev/pkg/github"
3232
"encr.dev/pkg/xos"
3333
daemonpb "encr.dev/proto/encore/daemon"
3434
)
3535

36-
const mcpJSON string = `{
37-
"mcpServers": {
38-
"encore-mcp": {
39-
"command": "encore",
40-
"args": ["mcp", "run", "--app={{ENCORE_APP_ID}}"]
41-
}
42-
}
43-
}
44-
`
45-
46-
const mdcTemplate string = `---
47-
description: Encore %s rules
48-
globs:
49-
alwaysApply: true
50-
---
51-
%s
52-
`
53-
5436
var (
5537
createAppTemplate string
5638
createAppOnPlatform bool
57-
createAppLang string
58-
createAppEditor = cmdutil.Oneof{
39+
createAppLang = cmdutil.Oneof{
5940
Value: "",
60-
Allowed: []string{"cursor"},
61-
Flag: "editor",
62-
FlagShort: "", // no short flag
63-
Desc: "Initialize the app for a cursor-based editor",
41+
Allowed: cmdutil.LanguageFlagValues(),
42+
Flag: "lang",
43+
FlagShort: "l",
44+
Desc: "Programming language to use for the app.",
45+
TypeDesc: "string",
46+
}
47+
createAppLLMRules = cmdutil.Oneof{
48+
Value: "",
49+
Allowed: llm_rules.LLMRulesFlagValues(),
50+
Flag: "llm-rules",
51+
FlagShort: "r",
52+
Desc: "Initialize the app with llm rules for a specific tool",
6453
TypeDesc: "string",
6554
}
66-
)
67-
68-
type editor string
69-
70-
const (
71-
EditorCursor editor = "cursor"
7255
)
7356

7457
var createAppCmd = &cobra.Command{
@@ -82,7 +65,19 @@ var createAppCmd = &cobra.Command{
8265
if len(args) > 0 {
8366
name = args[0]
8467
}
85-
if err := createApp(context.Background(), name, createAppTemplate, language(createAppLang), editor(createAppEditor.Value)); err != nil {
68+
69+
var tool llm_rules.Tool
70+
if createAppLLMRules.Value == "" {
71+
cfg, err := userconfig.Global().Get()
72+
if err != nil {
73+
cmdutil.Fatalf("Couldn't read user config: %s", err)
74+
}
75+
tool = llm_rules.Tool(cfg.LLMRules)
76+
} else {
77+
tool = llm_rules.Tool(createAppLLMRules.Value)
78+
}
79+
80+
if err := createApp(context.Background(), name, createAppTemplate, cmdutil.Language(createAppLang.Value), tool); err != nil {
8681
cmdutil.Fatal(err)
8782
}
8883
},
@@ -92,8 +87,8 @@ func init() {
9287
appCmd.AddCommand(createAppCmd)
9388
createAppCmd.Flags().BoolVar(&createAppOnPlatform, "platform", true, "whether to create the app with the Encore Platform")
9489
createAppCmd.Flags().StringVar(&createAppTemplate, "example", "", "URL to example code to use.")
95-
createAppCmd.Flags().StringVar(&createAppLang, "lang", "", "Programming language to use for the app. (ts, go)")
96-
createAppEditor.AddFlag(createAppCmd)
90+
createAppLang.AddFlag(createAppCmd)
91+
createAppLLMRules.AddFlag(createAppCmd)
9792
}
9893

9994
func promptAccountCreation() {
@@ -163,7 +158,7 @@ func promptRunApp() bool {
163158
}
164159

165160
// createApp is the implementation of the "encore app create" command.
166-
func createApp(ctx context.Context, name, template string, lang language, editor editor) (err error) {
161+
func createApp(ctx context.Context, name, template string, lang cmdutil.Language, llmRules llm_rules.Tool) (err error) {
167162
defer func() {
168163
// We need to send the telemetry synchronously to ensure it's sent before the command exits.
169164
telemetry.SendSync("app.create", map[string]any{
@@ -177,15 +172,15 @@ func createApp(ctx context.Context, name, template string, lang language, editor
177172

178173
promptAccountCreation()
179174

180-
if name == "" || template == "" {
181-
name, template, lang = selectTemplate(name, template, lang, false)
175+
if name == "" || template == "" || llmRules == "" {
176+
name, template, lang, llmRules = createAppForm(name, template, lang, llmRules, false)
182177
}
183178
// Treat the special name "empty" as the empty app template
184179
// (the rest of the code assumes that's the empty string).
185180
if template == "empty" {
186181
template = ""
187182
}
188-
if template == "" && lang == languageTS {
183+
if template == "" && lang == cmdutil.LanguageTS {
189184
template = "ts/empty"
190185
}
191186

@@ -286,7 +281,7 @@ func createApp(ctx context.Context, name, template string, lang language, editor
286281

287282
// Update to latest encore.dev release
288283
if _, err := os.Stat(filepath.Join(name, appRootRelpath, "go.mod")); err == nil {
289-
lang = languageGo
284+
lang = cmdutil.LanguageGo
290285
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
291286
s.Prefix = "Running go get encore.dev@latest"
292287
s.Start()
@@ -295,7 +290,7 @@ func createApp(ctx context.Context, name, template string, lang language, editor
295290
}
296291
s.Stop()
297292
} else if _, err := os.Stat(filepath.Join(name, appRootRelpath, "package.json")); err == nil {
298-
lang = languageTS
293+
lang = cmdutil.LanguageTS
299294
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
300295
s.Prefix = "Running npm install encore.dev@latest"
301296
s.Start()
@@ -336,23 +331,8 @@ func createApp(ctx context.Context, name, template string, lang language, editor
336331
color.Red("Failed to create app on daemon: %s\n", err)
337332
}
338333

339-
switch editor {
340-
case EditorCursor:
341-
cursorDir := filepath.Join(name, appRootRelpath, ".cursor")
342-
rulesDir := filepath.Join(cursorDir, "rules")
343-
err := os.MkdirAll(rulesDir, 0755)
344-
if err != nil {
345-
return err
346-
}
347-
err = os.WriteFile(filepath.Join(cursorDir, "mcp.json"), []byte(strings.ReplaceAll(mcpJSON, "{{ENCORE_APP_ID}}", appResp.AppId)), 0644)
348-
if err != nil {
349-
return err
350-
}
351-
llmInstructions, err := downloadLLMInstructions(lang)
352-
err = os.WriteFile(filepath.Join(rulesDir, "encore.mdc"), fmt.Appendf(nil, mdcTemplate, lang, string(llmInstructions)), 0644)
353-
if err != nil {
354-
return err
355-
}
334+
if err := llm_rules.SetupLLMRules(llmRules, lang, filepath.Join(name, appRootRelpath), appResp.AppId); err != nil {
335+
color.Red("Failed to setup LLM rules: %s\n", err)
356336
}
357337

358338
cmdutil.ClearTerminalExceptFirstNLines(0)
@@ -364,16 +344,7 @@ func createApp(ctx context.Context, name, template string, lang language, editor
364344
fmt.Printf("Web URL: %s%s", cyanf("https://app.encore.cloud/"+app.Slug), cmdutil.Newline)
365345
}
366346
fmt.Printf("App Root: %s\n", cyanf(appRoot))
367-
switch editor {
368-
case EditorCursor:
369-
fmt.Printf("MCP: %s\n", cyanf("Configured in Cursor"))
370-
fmt.Println()
371-
fmt.Println("Try these prompts in Cursor:")
372-
fmt.Println("→ \"add image uploads to my hello world app\"")
373-
fmt.Println("→ \"add a SQL database for storing user profiles\"")
374-
fmt.Println("→ \"add a pub/sub topic for sending notifications\"")
375-
}
376-
fmt.Println()
347+
llm_rules.PrintLLMRulesInfo(llmRules)
377348
greenBoldF := green.Add(color.Bold).SprintfFunc()
378349
fmt.Printf("Run your app with: %s\n", greenBoldF("cd %s && encore run", filepath.Join(name, appRootRelpath)))
379350
fmt.Println()
@@ -400,7 +371,7 @@ func createApp(ctx context.Context, name, template string, lang language, editor
400371
_, _ = cyan.Printf(" encore run\n")
401372
fmt.Print(" Run your app locally\n\n")
402373

403-
if lang == languageGo {
374+
if lang == cmdutil.LanguageGo {
404375
_, _ = cyan.Printf(" encore test ./...\n")
405376
} else {
406377
_, _ = cyan.Printf(" encore test\n")
@@ -416,44 +387,15 @@ func createApp(ctx context.Context, name, template string, lang language, editor
416387
return nil
417388
}
418389

419-
func downloadLLMInstructions(lang language) (string, error) {
420-
fmt.Println("Downloading LLM Instructions...")
421-
var url string
422-
switch lang {
423-
case languageGo:
424-
url = "https://raw.githubusercontent.com/encoredev/encore/refs/heads/main/go_llm_instructions.txt"
425-
case languageTS:
426-
url = "https://raw.githubusercontent.com/encoredev/encore/refs/heads/main/ts_llm_instructions.txt"
427-
default:
428-
return "", fmt.Errorf("unsupported language")
429-
}
430-
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
431-
s.Prefix = "Downloading LLM instructions..."
432-
s.Start()
433-
defer s.Stop()
434-
resp, err := http.Get(url)
435-
if err != nil {
436-
s.FinalMSG = fmt.Sprintf("failed, skipping: %v", err.Error())
437-
return "", err
438-
}
439-
defer resp.Body.Close()
440-
body, err := io.ReadAll(resp.Body)
441-
if err != nil {
442-
s.FinalMSG = fmt.Sprintf("failed, skipping: %v", err.Error())
443-
return "", err
444-
}
445-
return string(body), nil
446-
}
447-
448390
// detectLang attempts to detect the application language for an Encore application
449391
// situated at appRoot.
450-
func detectLang(appRoot string) language {
392+
func detectLang(appRoot string) cmdutil.Language {
451393
if _, err := os.Stat(filepath.Join(appRoot, "go.mod")); err == nil {
452-
return languageGo
394+
return cmdutil.LanguageGo
453395
} else if _, err := os.Stat(filepath.Join(appRoot, "package.json")); err == nil {
454-
return languageTS
396+
return cmdutil.LanguageTS
455397
}
456-
return languageGo
398+
return cmdutil.LanguageGo
457399
}
458400

459401
func validateName(name string) error {

0 commit comments

Comments
 (0)