diff --git a/bundle/bundle.go b/bundle/bundle.go index b5bb16e..723de2b 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -186,15 +186,35 @@ func copyIncludedFile(includedFilePath string, targetDir string) error { return fmt.Errorf("could not expand the path: %s\n%v", includedFilePath, err) } - // We can't do this here because it would result in files being removed iteratively. - // if err := file.SetupEmptyDir(targetDir); err != nil { - // return err - // } + // Get the source file's info to check permissions + fileInfo, err := os.Stat(expandedPath) + if err != nil { + return fmt.Errorf("could not stat file: %s\n%v", expandedPath, err) + } - cmd := exec.Command("cp", "-r", expandedPath, targetDir) + // Use cp with -p flag to preserve mode, ownership, timestamps + cmd := exec.Command("cp", "-pr", expandedPath, targetDir) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - return cmd.Run() + + // If the file is executable for user, group, or others, log it + if fileInfo.Mode()&0111 != 0 { + cmd2 := exec.Command("chmod", "+x", filepath.Join(targetDir, filepath.Base(expandedPath))) + cmd2.Stdout = os.Stdout + cmd2.Stderr = os.Stderr + log.Info("copying executable file", + "file", includedFilePath, + "mode", fileInfo.Mode().String(), + ) + err = cmd.Run() + if err != nil { + return err + } + return cmd2.Run() // TODO: This is not working. Could we move it to the binary directory and then run it from there? + } else { + return cmd.Run() + } + } // type FileManifest struct { diff --git a/cmd/init.go b/cmd/init.go index b854b44..253fc03 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -44,6 +44,9 @@ var samplePY []byte //go:embed init/greet.sh var sampleSH []byte +//go:embed init/greet.go +var sampleGO []byte + type TemplateVariable = struct { Name string Value *string @@ -70,14 +73,14 @@ var templateVariables map[string]TemplateVariable = map[string]TemplateVariable{ } // resetTemplateVariables resets all template variables to their default values -func resetTemplateVariables() { - cliName = "" - cliDescription = "A CLI tool" - cliVersion = "0.0.1" - cliAuthor = "Unknown" - cliLicense = "MIT" - cliLanguage = "javascript" -} +// func resetTemplateVariables() { +// cliName = "" +// cliDescription = "A CLI tool" +// cliVersion = "0.0.1" +// cliAuthor = "Unknown" +// cliLicense = "MIT" +// cliLanguage = "javascript" +// } // initCmd represents the init command var initCmd = &cobra.Command{ @@ -88,7 +91,7 @@ var initCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { // Reset template variables at the start of each run - resetTemplateVariables() + // resetTemplateVariables() // Determine name of the project log.Info("Initializing cmdeagle project") @@ -121,111 +124,121 @@ var initCmd = &cobra.Command{ // Only show interactive form if we're not in a test environment if os.Getenv("GO_TEST") != "1" { - form := huh.NewForm( - // Gather some final details about the order. - huh.NewGroup( - huh.NewInput(). - Title("What’s your CLI's name?."). - Description("This will be the name of the binary and the directory."). - // TODO implement rename command then change to: Description("This will be the name of the binary and the directory. You can change this later with `cmdeagle rename` command. It must be lowercase."). - Placeholder(templateVariables["name"].Placeholder). - Value(templateVariables["name"].Value). - Validate(func(str string) error { - if str == "" { - return fmt.Errorf("cli name cannot be empty") - } - - if !regexp.MustCompile(`^[a-z0-9\_\-]+$`).MatchString(str) { - return fmt.Errorf("cli name must be only lowercase letters(a-z), numbers(0-9), hypens( - ), and underscores( _ ) are allowed") - } - - return nil - }), - ), - huh.NewGroup( - huh.NewInput(). - Title("Description (optional)."). - Description("This will be the description of the binary and the directory."). - Placeholder(templateVariables["description"].Placeholder). - Value(templateVariables["description"].Value), - huh.NewInput(). - Title("Version (optional)."). - Placeholder(templateVariables["version"].Placeholder). - Value(templateVariables["version"].Value), - huh.NewInput(). - Title("Author (optional)."). - Placeholder(templateVariables["author"].Placeholder). - Value(templateVariables["author"].Value), - huh.NewInput(). - Title("License (optional)."). - Placeholder(templateVariables["license"].Placeholder). - Value(templateVariables["license"].Value), - ).Description("Some optional metadata to document your CLI. Displayed by the help command."), - // huh.NewGroup( - // huh.NewSelect[string](). - // Title("Choose a language/runtime to generate sample code for? (optional)."). - // Description("We'll generate sample code showing how to integrate languages/runtimes you choose."). - // Options( - // // huh.NewOption("go", "Go"), - // huh.NewOption("python", "Python"), - // // huh.NewOption("rust", "Rust"), - // huh.NewOption("javascript", "JavaScript"), - // // huh.NewOption("typescript", "TypeScript"), - // ). - // Value(&cliLanguages), - // ), - ) - - err = form.Run() + form := huh.NewForm( + // Gather some final details about the order. + huh.NewGroup( + huh.NewInput(). + Title("What’s your CLI's name?."). + Description("This will be the name of the binary and the directory."). + // TODO implement rename command then change to: Description("This will be the name of the binary and the directory. You can change this later with `cmdeagle rename` command. It must be lowercase."). + Placeholder(templateVariables["name"].Placeholder). + Value(templateVariables["name"].Value). + Validate(func(str string) error { + if str == "" { + return fmt.Errorf("cli name cannot be empty") + } + + if !regexp.MustCompile(`^[a-z0-9\_\-]+$`).MatchString(str) { + return fmt.Errorf("cli name must be only lowercase letters(a-z), numbers(0-9), hypens( - ), and underscores( _ ) are allowed") + } + + return nil + }), + ), + huh.NewGroup( + huh.NewInput(). + Title("Description (optional)."). + Description("This will be the description of the binary and the directory."). + Placeholder(templateVariables["description"].Placeholder). + Value(templateVariables["description"].Value), + huh.NewInput(). + Title("Version (optional)."). + Placeholder(templateVariables["version"].Placeholder). + Value(templateVariables["version"].Value), + huh.NewInput(). + Title("Author (optional)."). + Placeholder(templateVariables["author"].Placeholder). + Value(templateVariables["author"].Value), + huh.NewInput(). + Title("License (optional)."). + Placeholder(templateVariables["license"].Placeholder). + Value(templateVariables["license"].Value), + ).Description("Some optional metadata to document your CLI. Displayed by the help command."), + // huh.NewGroup( + // huh.NewSelect[string](). + // Title("Choose a language/runtime to generate sample code for? (optional)."). + // Description("We'll generate sample code showing how to integrate languages/runtimes you choose."). + // Options( + // // huh.NewOption("go", "Go"), + // huh.NewOption("python", "Python"), + // // huh.NewOption("rust", "Rust"), + // huh.NewOption("javascript", "JavaScript"), + // // huh.NewOption("typescript", "TypeScript"), + // ). + // Value(&cliLanguages), + // ), + ) + + err = form.Run() + + if err != nil { + log.Fatal(err) + } + } + // Create the .cmd.yaml file + fmt.Printf("Creating .cmd.yaml for %s\n", cliName) + _, err = os.Create(".cmd.yaml") if err != nil { - log.Fatal(err) + fmt.Printf("Error creating .cmd.yaml: %v\n", err) + return err } - } - // Create the .cmd.yaml file - fmt.Printf("Creating .cmd.yaml for %s\n", cliName) - _, err := os.Create(".cmd.yaml") - if err != nil { - fmt.Printf("Error creating .cmd.yaml: %v\n", err) - return err - } + // Replace template variables + interpolatedYAML := string(sampleYAMLConfig) + for name, variable := range templateVariables { + interpolatedYAML = strings.ReplaceAll(interpolatedYAML, templateVariablePrefix+name+templateVariableSuffix, *variable.Value) + } - // Replace template variables - interpolatedYAML := string(sampleYAMLConfig) - for name, variable := range templateVariables { - interpolatedYAML = strings.ReplaceAll(interpolatedYAML, templateVariablePrefix+name+templateVariableSuffix, *variable.Value) - } + // Write the file + err = os.WriteFile(".cmd.yaml", []byte(interpolatedYAML), 0644) + if err != nil { + log.Error("Failed to write .cmd.yaml", "error", err) + return fmt.Errorf("failed to write .cmd.yaml: %w", err) + } - // Write the file - err = os.WriteFile(".cmd.yaml", []byte(interpolatedYAML), 0644) - if err != nil { - log.Error("Failed to write .cmd.yaml", "error", err) - return fmt.Errorf("failed to write .cmd.yaml: %w", err) - } + // Create the greet.sh file + err = os.WriteFile("greet.sh", sampleSH, 0644) + if err != nil { + log.Error("Failed to write greet.sh", "error", err) + return fmt.Errorf("failed to write greet.sh: %w", err) + } - // Create the greet.sh file - err = os.WriteFile("greet.sh", sampleSH, 0644) - if err != nil { - log.Error("Failed to write greet.sh", "error", err) - return fmt.Errorf("failed to write greet.sh: %w", err) - } + // Create the greet.js file + err = os.WriteFile("greet.js", sampleJS, 0644) + if err != nil { + log.Error("Failed to write greet.js", "error", err) + return fmt.Errorf("failed to write greet.js: %w", err) + } - // Create the greet.js file - err = os.WriteFile("greet.js", sampleJS, 0644) - if err != nil { - log.Error("Failed to write greet.js", "error", err) - return fmt.Errorf("failed to write greet.js: %w", err) - } + // Create the greet.py file + err = os.WriteFile("greet.py", samplePY, 0644) + if err != nil { + log.Error("Failed to write greet.py", "error", err) + return fmt.Errorf("failed to write greet.py: %w", err) + } + + // Create the greet.go file + err = os.WriteFile("greet.go", sampleGO, 0644) + if err != nil { + log.Error("Failed to write greet.go", "error", err) + return fmt.Errorf("failed to write greet.go: %w", err) + } - // Create the greet.py file - err = os.WriteFile("greet.py", samplePY, 0644) - if err != nil { - log.Error("Failed to write greet.py", "error", err) - return fmt.Errorf("failed to write greet.py: %w", err) + log.Info("Successfully initialized cmdeagle project", "name", cliName) + return nil } - log.Info("Successfully initialized cmdeagle project", "name", cliName) return nil }, } diff --git a/cmd/init/greet.go b/cmd/init/greet.go new file mode 100644 index 0000000..484dd30 --- /dev/null +++ b/cmd/init/greet.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "os" + "strconv" + "strings" +) + +func main() { + // Get arguments from environment variables + name := getEnv("ARGS_NAME") + age := getEnv("ARGS_AGE") + + // Get flags from environment variables + uppercase := getEnv("FLAGS_UPPERCASE") == "true" + lowercase := getEnv("FLAGS_LOWERCASE") == "true" + repeat, _ := strconv.Atoi(getEnv("FLAGS_REPEAT")) + + // Construct base greeting + greeting := fmt.Sprintf("Hello %s!", name) + if age != "" { + greeting += fmt.Sprintf(" You are %s years old.", age) + } + + // Apply case transformations + if uppercase { + greeting = strings.ToUpper(greeting) + } else if lowercase { + greeting = strings.ToLower(greeting) + } + + // Output greeting with repetition + for i := 0; i < repeat; i++ { + fmt.Println(greeting) + } +} + +func getEnv(key string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return "" +} diff --git a/cmd/init/template.cmd.yaml b/cmd/init/template.cmd.yaml index fffedb3..7e14462 100644 --- a/cmd/init/template.cmd.yaml +++ b/cmd/init/template.cmd.yaml @@ -25,6 +25,10 @@ includes: - "./greet.sh" - "./greet.js" - "./greet.py" +- "{{name}}-go-binary" + +build: | + go build -o {{name}}-go-binary greet.go && chmod +x {{name}}-go-binary # The subcommands of your CLI. You can nest subcommands as deeply as you want. # Each subcommand can also declare its own `requires`, `includes` just like the root command above. @@ -49,6 +53,7 @@ commands: # - `<=` (less than or equal) # - No operator (exact match) + # The arguments of your CLI. args: vars: - name: name @@ -64,10 +69,10 @@ commands: # max: 100 depends-on: - name: name - rules: - # Rules apply to all arguments and they follow the same naming conventions as the ones from Cobra - minimum-n-args: 1 - maximum-n-args: 2 + # rules: + # # Rules apply to all arguments and they follow the same naming conventions as the ones from Cobra + # - minimum-n-args: 1 + # maximum-n-args: 2 flags: @@ -107,11 +112,30 @@ commands: conflicts-with: - use-python + - name: use-go + shorthand: js + type: boolean + description: "Use Go to greet the user" + conflicts-with: + - use-python + # - name: use + # type: string + # description: "Use the specified language to greet the user" + # default: "sh" + # constraints: + # - in: + # - "sh" + # - "js" + # - "python" + # - "go" + start: | if [ "${flags.use-python}" = "true" ]; then python3 greet.py elif [ "${flags.use-js}" = "true" ]; then node greet.js + elif [ "${flags.use-go}" = "true" ]; then + ./{{name}}-go-binary else sh greet.sh fi diff --git a/examples/mycli/.cmd.yaml b/examples/mycli/.cmd.yaml deleted file mode 100644 index a2a4e52..0000000 --- a/examples/mycli/.cmd.yaml +++ /dev/null @@ -1,120 +0,0 @@ -# This is the name of the binary you use to invoke your CLI. - -name: "mycli" - -# Some optional metadata you can add to your CLI. This doesn't get used anywhere but may be useful to document. -# Displayed by `mycli --help` when help command invoked in some other way. -description: "A CLI tool" # e.g. "My CLI is a tool to manage my projects." -version: "0.0.1" # e.g. "0.0.1" -author: "Unknown" # e.g. Your name e.g. "John Doe" -license: "MIT" # e.g. Your license e.g. "MIT" - -# When you run `cmdeagle build` without arguments, we'll try to install the binary in these directories -# - For macOs and Linux: /usr/local/bin or ~/.local/bin -# - For Windows: C:\Users\\AppData\Local\Programs\MyApp\bin - -# We also install the bundled files declared by your CLI in these directories -# - For macOS and Linux: /usr/local/share/mycli or ~/.local/share/mycli -# - Windows (user-only): %LocalAppData%\MyApp - - - -includes: -# You can bundle files with your CLI by declaring the paths to them in the `includes` field. -# This is useful for things like static assets, media, configuration files, data files, etc. -- "./greet.sh" -- "./greet.js" -- "./greet.py" - -# The subcommands of your CLI. You can nest subcommands as deeply as you want. -# Each subcommand can also declare its own `requires`, `includes` just like the root command above. -commands: - -- name: greet - description: "Greet the user." - - requires: - # The `requires` statement is used to specify the dependencies your CLI needs to run. These are checked when you run - # your binary. They are not checked at build time. - # You can use comparison operators preceeding a version number to specify the version of the dependency you need. - node: ">=16.0.0" - python3: "*" - # Here is the full list of operators you can use: - # - `*` (any version) - # - `^` (major version) - # - `~` (major and minor version) - # - `>` (greater than) - # - `<` (less than) - # - `>=` (greater than or equal) - # - `<=` (less than or equal) - # - No operator (exact match) - - args: - vars: - - - name: name - type: string - description: "Name to greet" - default: "World" - required: true - - - name: age - type: number - description: "Age of the user" - constraints: - # TODO: Fix this - min: 18 - max: 100 - depends-on: - - name: name - # rules: - # # Rules apply to all arguments and they follow the same naming conventions as the ones from Cobra - # - minimum-n-args: 1 - # maximum-n-args: 2 - - flags: - - - name: uppercase - shorthand: u - type: boolean - description: "Convert greeting to uppercase" - default: false - conflicts-with: - - lowercase - - - name: lowercase - shorthand: u - type: boolean - description: "Convert greeting to lowercase" - default: false - conflicts-with: - - uppercase - - - name: repeat - shorthand: r - type: number - description: "Repeat the greeting n times" - default: 1 - - - name: use-python - shorthand: py - type: boolean - description: "Use Python to greet the user" - conflicts-with: - - use-javascript - - - name: use-js - shorthand: js - type: boolean - description: "Use JavaScript to greet the user" - conflicts-with: - - use-python - - start: | - if [ "${flags.use-python}" = "true" ]; then - python3 greet.py - elif [ "${flags.use-js}" = "true" ]; then - node greet.js - else - sh greet.sh - fi diff --git a/examples/mycli/greet.js b/examples/mycli/greet.js deleted file mode 100644 index 7243196..0000000 --- a/examples/mycli/greet.js +++ /dev/null @@ -1,26 +0,0 @@ -// Get arguments from environment variables -const name = process.env.ARGS_NAME || 'World'; -const age = process.env.ARGS_AGE; - -// Get flags from environment variables -const uppercase = process.env.FLAGS_UPPERCASE === 'true'; -const lowercase = process.env.FLAGS_LOWERCASE === 'true'; -const repeat = parseInt(process.env.FLAGS_REPEAT || '1'); - -// Construct base greeting -let greeting = `Hello ${name}!`; -if (age) { - greeting += ` You are ${age} years old.`; -} - -// Apply case transformations -if (uppercase) { - greeting = greeting.toUpperCase(); -} else if (lowercase) { - greeting = greeting.toLowerCase(); -} - -// Output greeting with repetition -for (let i = 0; i < repeat; i++) { - console.log(greeting); -} diff --git a/examples/mycli/greet.py b/examples/mycli/greet.py deleted file mode 100644 index 47f8462..0000000 --- a/examples/mycli/greet.py +++ /dev/null @@ -1,25 +0,0 @@ -import os - -# Get arguments from environment variables -name = os.environ.get('ARGS_NAME', 'World') -age = os.environ.get('ARGS_AGE') - -# Get flags from environment variables -uppercase = os.environ.get('FLAGS_UPPERCASE', 'false').lower() == 'true' -lowercase = os.environ.get('FLAGS_LOWERCASE', 'false').lower() == 'true' -repeat = int(os.environ.get('FLAGS_REPEAT', '1')) - -# Construct base greeting -greeting = f"Hello {name}!" -if age: - greeting += f" You are {age} years old." - -# Apply case transformations -if uppercase: - greeting = greeting.upper() -elif lowercase: - greeting = greeting.lower() - -# Output greeting with repetition -for _ in range(repeat): - print(greeting) diff --git a/examples/mycli/greet.sh b/examples/mycli/greet.sh deleted file mode 100644 index 583f59a..0000000 --- a/examples/mycli/greet.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -# Get arguments from environment variables -name=${ARGS_NAME:-World} -age=$ARGS_AGE - -# Get flags from environment variables (convert string to boolean/number) -uppercase=$([ "$FLAGS_UPPERCASE" = "true" ] && echo "true" || echo "false") -lowercase=$([ "$FLAGS_LOWERCASE" = "true" ] && echo "true" || echo "false") -repeat=${FLAGS_REPEAT:-1} - -# Construct base greeting -greeting="Hello $name!" -if [ -n "$age" ]; then - greeting="$greeting You are $age years old." -fi - -# Apply case transformations -if [ "$uppercase" = "true" ]; then - greeting=$(echo "$greeting" | tr '[:lower:]' '[:upper:]') -elif [ "$lowercase" = "true" ]; then - greeting=$(echo "$greeting" | tr '[:upper:]' '[:lower:]') -fi - -# Output greeting with repetition -i=0 -while [ $i -lt $repeat ]; do - echo "$greeting" - i=$((i + 1)) -done \ No newline at end of file