Skip to content

Commit 703b106

Browse files
authored
Merge pull request #381 from defang-io/lio-templates
Instantiate a project from a sample
2 parents bbbf9c3 + 0ea5e64 commit 703b106

File tree

4 files changed

+165
-20
lines changed

4 files changed

+165
-20
lines changed

src/cmd/cli/command/commands.go

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -424,16 +424,47 @@ var generateCmd = &cobra.Command{
424424
return errors.New("cannot run in non-interactive mode")
425425
}
426426

427+
var language string
428+
if err := survey.AskOne(&survey.Select{
429+
Message: "Choose the language you'd like to use:",
430+
Options: []string{"Nodejs", "Golang", "Python"},
431+
Default: "Nodejs",
432+
Help: "The project code will be in the language you choose here.",
433+
}, &language); err != nil {
434+
return err
435+
}
436+
437+
var category, sample string
438+
439+
// Fetch the list of samples from the Defang repository
440+
if samples, err := cli.FetchSamples(cmd.Context()); err != nil {
441+
term.Debug(" - unable to fetch samples:", err)
442+
} else if len(samples) > 0 {
443+
const generateWithAI = "Generate with AI"
444+
445+
category = strings.ToLower(language)
446+
sampleNames := []string{generateWithAI}
447+
// sampleDescriptions := []string{"Generate a sample from scratch using a language prompt"}
448+
for _, sample := range samples {
449+
if sample.Category == category {
450+
sampleNames = append(sampleNames, sample.Name)
451+
// sampleDescriptions = append(sampleDescriptions, sample.Readme)
452+
}
453+
}
454+
455+
if err := survey.AskOne(&survey.Select{
456+
Message: "Choose a sample service:",
457+
Options: sampleNames,
458+
Help: "The project code will be based on the sample you choose here.",
459+
}, &sample); err != nil {
460+
return err
461+
}
462+
if sample == generateWithAI {
463+
sample = ""
464+
}
465+
}
466+
427467
var qs = []*survey.Question{
428-
{
429-
Name: "language",
430-
Prompt: &survey.Select{
431-
Message: "Choose the language you'd like to use:",
432-
Options: []string{"Nodejs", "Golang", "Python"},
433-
Default: "Nodejs",
434-
Help: "The generated code will be in the language you choose here.",
435-
},
436-
},
437468
{
438469
Name: "description",
439470
Prompt: &survey.Input{
@@ -457,13 +488,16 @@ Generate will write files in the current folder. You can edit them and then depl
457488
},
458489
}
459490

491+
if sample != "" {
492+
qs = qs[1:] // user picked a sample, so we skip the description question
493+
}
494+
460495
prompt := struct {
461-
Language string // or you can tag fields to match a specific name
462-
Description string
496+
Description string // or you can tag fields to match a specific name
463497
Folder string
464498
}{}
465499

466-
// ask the questions
500+
// ask the remaining questions
467501
err := survey.Ask(qs, &prompt)
468502
if err != nil {
469503
return err
@@ -479,7 +513,7 @@ Generate will write files in the current folder. You can edit them and then depl
479513
}
480514
}
481515

482-
Track("Generate Started", P{"language", prompt.Language}, P{"description", prompt.Description}, P{"folder", prompt.Folder})
516+
Track("Generate Started", P{"language", language}, P{"sample", sample}, P{"description", prompt.Description}, P{"folder", prompt.Folder})
483517

484518
// create the folder if needed
485519
cd := ""
@@ -496,10 +530,18 @@ Generate will write files in the current folder. You can edit them and then depl
496530
term.Warn(" ! The folder is not empty. Files may be overwritten. Press Ctrl+C to abort.")
497531
}
498532

499-
term.Info(" * Working on it. This may take 1 or 2 minutes...")
500-
_, err = cli.Generate(cmd.Context(), client, prompt.Language, prompt.Description)
501-
if err != nil {
502-
return err
533+
if prompt.Description != "" {
534+
term.Info(" * Working on it. This may take 1 or 2 minutes...")
535+
_, err := cli.GenerateWithAI(cmd.Context(), client, language, prompt.Description)
536+
if err != nil {
537+
return err
538+
}
539+
} else {
540+
term.Info(" * Fetching sample from the Defang repository...")
541+
err := cli.InitFromSample(cmd.Context(), category, sample)
542+
if err != nil {
543+
return err
544+
}
503545
}
504546

505547
term.Info(" * Code generated successfully in folder", prompt.Folder)

src/pkg/cli/generate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
defangv1 "github.com/defang-io/defang/src/protos/io/defang/v1"
1111
)
1212

13-
func Generate(ctx context.Context, client client.Client, language string, description string) ([]string, error) {
13+
func GenerateWithAI(ctx context.Context, client client.Client, language string, description string) ([]string, error) {
1414
if DoDryRun {
1515
term.Warn(" ! Dry run, not generating files")
1616
return nil, ErrDryRun

src/pkg/cli/init.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package cli
2+
3+
import (
4+
"archive/tar"
5+
"compress/gzip"
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
"os"
11+
"strings"
12+
13+
"github.com/defang-io/defang/src/pkg/http"
14+
"github.com/defang-io/defang/src/pkg/term"
15+
)
16+
17+
type Sample struct {
18+
Name string `json:"name"`
19+
Category string `json:"category"` // language
20+
Readme string `json:"readme"`
21+
}
22+
23+
func FetchSamples(ctx context.Context) ([]Sample, error) {
24+
resp, err := http.GetWithHeader(ctx, "https://docs.defang.io/samples.json", http.Header{"Accept-Encoding": []string{"gzip"}})
25+
if err != nil {
26+
return nil, err
27+
}
28+
defer resp.Body.Close()
29+
term.Debug(resp.Header)
30+
reader := resp.Body
31+
if resp.Header.Get("Content-Encoding") == "gzip" {
32+
reader, err = gzip.NewReader(resp.Body)
33+
if err != nil {
34+
return nil, err
35+
}
36+
defer reader.Close()
37+
}
38+
var samples []Sample
39+
err = json.NewDecoder(reader).Decode(&samples)
40+
return samples, err
41+
}
42+
43+
func InitFromSample(ctx context.Context, category, sample string) error {
44+
const repo = "defang"
45+
const branch = "main"
46+
47+
prefix := fmt.Sprintf("%s-%s/samples/%s/%s/", repo, branch, category, sample)
48+
resp, err := http.GetWithContext(ctx, "https://github.com/defang-io/"+repo+"/archive/refs/heads/"+branch+".tar.gz")
49+
if err != nil {
50+
return err
51+
}
52+
defer resp.Body.Close()
53+
term.Debug(resp.Header)
54+
body, err := gzip.NewReader(resp.Body)
55+
if err != nil {
56+
return err
57+
}
58+
defer body.Close()
59+
tarReader := tar.NewReader(body)
60+
term.Info(" * Writing files to disk...")
61+
for {
62+
h, err := tarReader.Next()
63+
if err != nil {
64+
if err == io.EOF {
65+
break
66+
}
67+
return err
68+
}
69+
70+
if base, ok := strings.CutPrefix(h.Name, prefix); ok && len(base) > 0 {
71+
fmt.Println(" -", base)
72+
if h.FileInfo().IsDir() {
73+
if err := os.MkdirAll(base, 0755); err != nil {
74+
return err
75+
}
76+
continue
77+
}
78+
// Like os.Create, but with the same mode as the original file (so scripts are executable, etc.)
79+
file, err := os.OpenFile(base, os.O_RDWR|os.O_CREATE|os.O_TRUNC, h.FileInfo().Mode())
80+
if err != nil {
81+
return err
82+
}
83+
if _, err := io.Copy(file, tarReader); err != nil {
84+
return err
85+
}
86+
}
87+
}
88+
return nil
89+
}

src/pkg/http/get.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,25 @@ import (
55
"net/http"
66
)
77

8-
func GetWithAuth(ctx context.Context, url, auth string) (*http.Response, error) {
8+
type Header = http.Header
9+
10+
func GetWithContext(ctx context.Context, url string) (*http.Response, error) {
11+
hreq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
12+
if err != nil {
13+
return nil, err
14+
}
15+
return http.DefaultClient.Do(hreq)
16+
}
17+
18+
func GetWithHeader(ctx context.Context, url string, header http.Header) (*http.Response, error) {
919
hreq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
1020
if err != nil {
1121
return nil, err
1222
}
13-
hreq.Header.Set("Authorization", auth)
23+
hreq.Header = header
1424
return http.DefaultClient.Do(hreq)
1525
}
26+
27+
func GetWithAuth(ctx context.Context, url, auth string) (*http.Response, error) {
28+
return GetWithHeader(ctx, url, http.Header{"Authorization": []string{auth}})
29+
}

0 commit comments

Comments
 (0)