Skip to content

Commit e1355d6

Browse files
authored
Merge pull request #605 from DefangLabs/lio-more-help
add more help info to the CLI
2 parents fab0ce7 + b480aaf commit e1355d6

File tree

6 files changed

+88
-29
lines changed

6 files changed

+88
-29
lines changed

src/cmd/cli/command/commands.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ func SetupCommands(version string) {
173173

174174
// Generate Command
175175
RootCmd.AddCommand(generateCmd)
176+
RootCmd.AddCommand(newCmd)
176177

177178
// Get Services Command
178179
getServicesCmd.Flags().BoolP("long", "l", false, "show more details")
@@ -214,9 +215,17 @@ func SetupCommands(version string) {
214215
composeCmd.AddCommand(composeDownCmd)
215216
composeStartCmd.Flags().Bool("force", false, "force a build of the image even if nothing has changed")
216217
composeCmd.AddCommand(composeStartCmd)
217-
RootCmd.AddCommand(composeCmd)
218218
composeCmd.AddCommand(composeRestartCmd)
219219
composeCmd.AddCommand(composeStopCmd)
220+
composeCmd.AddCommand(getServicesCmd) // like docker compose ls
221+
RootCmd.AddCommand(composeCmd)
222+
223+
// Add up/down commands to the root as well
224+
RootCmd.AddCommand(composeDownCmd)
225+
RootCmd.AddCommand(composeUpCmd)
226+
// RootCmd.AddCommand(composeStartCmd)
227+
// RootCmd.AddCommand(composeRestartCmd)
228+
// RootCmd.AddCommand(composeStopCmd)
220229

221230
// Debug Command
222231
debugCmd.Flags().String("etag", "", "deployment ID (ETag) of the service")
@@ -270,7 +279,7 @@ var RootCmd = &cobra.Command{
270279
SilenceErrors: true,
271280
Use: "defang",
272281
Args: cobra.NoArgs,
273-
Short: "Defang CLI manages services on the Defang cluster",
282+
Short: "Defang CLI is used to develop, deploy, and debug your cloud services",
274283
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
275284

276285
term.SetDebug(doDebug)
@@ -430,10 +439,10 @@ var certGenerateCmd = &cobra.Command{
430439
}
431440

432441
var generateCmd = &cobra.Command{
433-
Use: "generate [SAMPLE]",
442+
Use: "generate",
434443
Args: cobra.MaximumNArgs(1),
435-
Aliases: []string{"gen", "new", "init"},
436-
Short: "Generate a sample Defang project in the current folder",
444+
Aliases: []string{"gen"},
445+
Short: "Generate a sample Defang project",
437446
RunE: func(cmd *cobra.Command, args []string) error {
438447
var sample, language, defaultFolder string
439448
if len(args) > 0 {
@@ -446,12 +455,12 @@ var generateCmd = &cobra.Command{
446455
}
447456
return cli.InitFromSamples(cmd.Context(), "", []string{sample})
448457
}
458+
449459
sampleList, fetchSamplesErr := cli.FetchSamples(cmd.Context())
450460
if sample == "" {
451461
if err := survey.AskOne(&survey.Select{
452462
Message: "Choose the language you'd like to use:",
453-
Options: []string{"Nodejs", "Golang", "Python"},
454-
Default: "Nodejs",
463+
Options: cli.SupportedLanguages,
455464
Help: "The project code will be in the language you choose here.",
456465
}, &language); err != nil {
457466
return err
@@ -604,6 +613,14 @@ var generateCmd = &cobra.Command{
604613
},
605614
}
606615

616+
var newCmd = &cobra.Command{
617+
Use: "new [SAMPLE]",
618+
Args: cobra.MaximumNArgs(1),
619+
Aliases: []string{"init"},
620+
Short: "Create a new Defang project from a sample",
621+
RunE: generateCmd.RunE,
622+
}
623+
607624
func collectUnsetEnvVars(project *proj.Project) []string {
608625
var envVars []string
609626
if project != nil {
@@ -669,6 +686,7 @@ var getVersionCmd = &cobra.Command{
669686
var tailCmd = &cobra.Command{
670687
Use: "tail",
671688
Annotations: authNeededAnnotation,
689+
Aliases: []string{"logs"},
672690
Args: cobra.NoArgs,
673691
Short: "Tail logs from one or more services",
674692
RunE: func(cmd *cobra.Command, args []string) error {
@@ -775,7 +793,7 @@ var configSetCmd = &cobra.Command{
775793
}
776794
term.Info("Updated value for", name)
777795

778-
printDefangHint("To update the deployed values, do:", "compose start")
796+
printDefangHint("To update the deployed values, do:", "compose restart")
779797
return nil
780798
},
781799
}
@@ -818,20 +836,33 @@ var composeCmd = &cobra.Command{
818836
Aliases: []string{"stack"},
819837
Args: cobra.NoArgs,
820838
Short: "Work with local Compose files",
839+
Long: `Define and deploy multi-container applications with Defang. Most compose commands require
840+
a "compose.yaml" file. The simplest "compose.yaml" file with a single service is:
841+
842+
services:
843+
app: # the name of the service
844+
build: . # the folder with the Dockerfile and app sources (. means current folder)
845+
ports:
846+
- 80 # the port the service listens on for HTTP requests
847+
`,
821848
}
822849

823850
var composeUpCmd = &cobra.Command{
824851
Use: "up",
825852
Annotations: authNeededAnnotation,
826853
Args: cobra.NoArgs, // TODO: takes optional list of service names
827-
Short: "Like 'start' but immediately tracks the progress of the deployment",
854+
Short: "Reads a Compose file and deploy a new project or update an existing project",
828855
RunE: func(cmd *cobra.Command, args []string) error {
829856
var force, _ = cmd.Flags().GetBool("force")
830857
var detach, _ = cmd.Flags().GetBool("detach")
831858

832859
since := time.Now()
833860
deploy, project, err := cli.ComposeUp(cmd.Context(), client, force)
834861
if err != nil {
862+
if !errors.Is(err, types.ErrComposeFileNotFound) {
863+
return err
864+
}
865+
printDefangHint("To start a new project, do:", "new")
835866
return err
836867
}
837868

@@ -1019,7 +1050,7 @@ var composeDownCmd = &cobra.Command{
10191050
Aliases: []string{"rm"},
10201051
Annotations: authNeededAnnotation,
10211052
Args: cobra.NoArgs, // TODO: takes optional list of service names
1022-
Short: "Like 'stop' but also deprovisions the services from the cluster",
1053+
Short: "Reads a Compose file and deprovisions its services",
10231054
RunE: func(cmd *cobra.Command, args []string) error {
10241055
var detach, _ = cmd.Flags().GetBool("detach")
10251056

src/pkg/cli/compose/validation.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
compose "github.com/compose-spec/compose-go/v2/types"
1414
)
1515

16+
var ErrDockerfileNotFound = errors.New("dockerfile not found")
17+
1618
func ValidateProject(project *compose.Project) error {
1719
if project == nil {
1820
return errors.New("no project found")
@@ -111,7 +113,7 @@ func ValidateProject(project *compose.Project) error {
111113
// Check if the dockerfile exists
112114
dockerfilePath := filepath.Join(svccfg.Build.Context, svccfg.Build.Dockerfile)
113115
if _, err := os.Stat(dockerfilePath); err != nil {
114-
return fmt.Errorf("service %q: dockerfile not found: %q", svccfg.Name, dockerfilePath)
116+
return fmt.Errorf("service %q: %w: %q", svccfg.Name, ErrDockerfileNotFound, dockerfilePath)
115117
}
116118
}
117119
if svccfg.Build.SSH != nil {

src/pkg/cli/debug.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,24 @@ func findMatchingProjectFiles(project *types.Project, services []string) []*defa
120120
return files
121121
}
122122

123+
func IsProjectFile(basename string) bool {
124+
return filepathMatchAny(patterns, basename)
125+
}
126+
127+
func filepathMatchAny(patterns []string, name string) bool {
128+
for _, pattern := range patterns {
129+
matched, err := filepath.Match(pattern, name)
130+
if err != nil {
131+
term.Debug("error matching pattern:", err)
132+
continue
133+
}
134+
if matched {
135+
return true // file matched, no need to check other patterns
136+
}
137+
}
138+
return false
139+
}
140+
123141
func findMatchingFiles(folder, dockerfile string) []*defangv1.File {
124142
var files []*defangv1.File
125143

@@ -137,17 +155,9 @@ func findMatchingFiles(folder, dockerfile string) []*defangv1.File {
137155
return errFileLimitReached
138156
}
139157

140-
for _, pattern := range patterns {
141-
matched, err := filepath.Match(pattern, info.Name())
142-
if err != nil {
143-
term.Debug("error matching pattern:", err)
144-
continue
145-
}
146-
if matched {
147-
if file := readFile(path); file != nil {
148-
files = append(files, file)
149-
break // file matched, no need to check other patterns
150-
}
158+
if IsProjectFile(info.Name()) {
159+
if file := readFile(path); file != nil {
160+
files = append(files, file)
151161
}
152162
}
153163
return nil

src/pkg/cli/generate.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
1212
)
1313

14+
var SupportedLanguages = []string{"Nodejs", "Golang", "Python"}
15+
1416
func GenerateWithAI(ctx context.Context, client client.Client, language, dir, description string) ([]string, error) {
1517
if DoDryRun {
1618
term.Warn("Dry run, not generating files")

src/pkg/cli/new.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,17 @@ func FetchSamples(ctx context.Context) ([]Sample, error) {
5050
return samples, err
5151
}
5252

53+
// MixinFromSamples copies the sample files into the given directory, skipping existing files.
54+
func MixinFromSample(ctx context.Context, dir string, name string) error {
55+
return copyFromSamples(ctx, dir, []string{name}, true)
56+
}
57+
58+
// InitFromSamples copies the sample(s) into the given directory, aborting if any files already exist.
5359
func InitFromSamples(ctx context.Context, dir string, names []string) error {
60+
return copyFromSamples(ctx, dir, names, false)
61+
}
62+
63+
func copyFromSamples(ctx context.Context, dir string, names []string, skipExisting bool) error {
5464
const repo = "samples"
5565
const branch = "main"
5666

@@ -99,8 +109,12 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error {
99109
}
100110
continue
101111
}
102-
if err := createFile(path, h, tarReader); err != nil {
103-
return err
112+
// Use the same mode as the original file (so scripts are executable, etc.)
113+
if err := writeFileExcl(path, tarReader, h.FileInfo().Mode()); err != nil {
114+
if !skipExisting || !os.IsExist(err) {
115+
return err
116+
}
117+
term.Warnf("File %q already exists, skipping", path)
104118
}
105119
}
106120
}
@@ -111,14 +125,14 @@ func InitFromSamples(ctx context.Context, dir string, names []string) error {
111125
return nil
112126
}
113127

114-
func createFile(base string, h *tar.Header, tarReader *tar.Reader) error {
115-
// Like os.Create, but with the same mode as the original file (so scripts are executable, etc.)
116-
file, err := os.OpenFile(base, os.O_RDWR|os.O_CREATE|os.O_EXCL, h.FileInfo().Mode())
128+
// writeFileExcl is like os.WriteFile, but with O_EXCL to avoid overwriting existing files.
129+
func writeFileExcl(base string, reader io.Reader, mode os.FileMode) error {
130+
file, err := os.OpenFile(base, os.O_RDWR|os.O_CREATE|os.O_EXCL, mode)
117131
if err != nil {
118132
return err
119133
}
120134
defer file.Close()
121-
if _, err := io.Copy(file, tarReader); err != nil {
135+
if _, err := io.Copy(file, reader); err != nil {
122136
return err
123137
}
124138
return nil

src/pkg/types/error.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ package types
22

33
import "errors"
44

5-
var ErrComposeFileNotFound = errors.New("no compose file found")
5+
var ErrComposeFileNotFound = errors.New("no compose.yaml file found")

0 commit comments

Comments
 (0)