diff --git a/cmd/lk/app.go b/cmd/lk/app.go index f6efcb06..39b817f2 100644 --- a/cmd/lk/app.go +++ b/cmd/lk/app.go @@ -94,12 +94,32 @@ var ( Action: runTask, }, { - Hidden: true, - Name: "env", - Usage: "Manage environment variables", - Before: requireProject, + Name: "env", + Usage: "Print project environment variables expanded from a .env.example file", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "w", + Aliases: []string{"write"}, + Usage: "Write environment variables to .env.local file", + }, + }, + ArgsUsage: "[DIR] location of the project directory (default: current directory)", + Before: requireProject, Action: func(ctx context.Context, cmd *cli.Command) error { - return instantiateEnv(ctx, cmd, ".", nil) + rootDir := cmd.Args().First() + if rootDir == "" { + rootDir = "." + } + + env, err := instantiateEnv(ctx, cmd, rootDir, nil) + if err != nil { + return err + } + if cmd.Bool("write") { + return bootstrap.WriteDotEnv(rootDir, env) + } else { + return bootstrap.PrintDotEnv(env) + } }, }, }, @@ -254,9 +274,11 @@ func setupTemplate(ctx context.Context, cmd *cli.Command) error { fmt.Println("Instantiating environment...") addlEnv := &map[string]string{"LIVEKIT_SANDBOX_ID": sandboxID} - if err := instantiateEnv(ctx, cmd, appName, addlEnv); err != nil { + env, err := instantiateEnv(ctx, cmd, appName, addlEnv) + if err != nil { return err } + bootstrap.WriteDotEnv(appName, env) if install { fmt.Println("Installing template...") @@ -307,7 +329,7 @@ func cleanupTemplate(ctx context.Context, cmd *cli.Command, appName string) erro return bootstrap.CleanupTemplate(appName) } -func instantiateEnv(ctx context.Context, cmd *cli.Command, rootPath string, addlEnv *map[string]string) error { +func instantiateEnv(ctx context.Context, cmd *cli.Command, rootPath string, addlEnv *map[string]string) (map[string]string, error) { env := map[string]string{ "LIVEKIT_API_KEY": project.APIKey, "LIVEKIT_API_SECRET": project.APISecret, @@ -350,6 +372,9 @@ func doPostCreate(ctx context.Context, _ *cli.Command, rootPath string, verbose if err != nil { return err } + if tf == nil { + return nil + } task, err := bootstrap.NewTask(ctx, tf, rootPath, string(bootstrap.TaskPostCreate), verbose) if task == nil || err != nil { diff --git a/pkg/bootstrap/bootstrap.go b/pkg/bootstrap/bootstrap.go index 293188a3..a4421607 100644 --- a/pkg/bootstrap/bootstrap.go +++ b/pkg/bootstrap/bootstrap.go @@ -21,13 +21,13 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "io/fs" "net/http" "os" "os/exec" "path" - "path/filepath" "runtime" "strings" @@ -122,7 +122,14 @@ func FetchSandboxDetails(ctx context.Context, sid, token, serverURL string) (*Sa } func ParseTaskfile(rootPath string) (*ast.Taskfile, error) { - file, err := os.ReadFile(path.Join(rootPath, TaskFile)) + taskfilePath := path.Join(rootPath, TaskFile) + + // taskfile.yaml is optional + if _, err := os.Stat(taskfilePath); err != nil && errors.Is(err, fs.ErrNotExist) { + return nil, nil + } + + file, err := os.ReadFile(taskfilePath) if err != nil { return nil, err } @@ -180,54 +187,62 @@ func NewTask(ctx context.Context, tf *ast.Taskfile, dir, taskName string, verbos type PromptFunc func(key string, value string) (string, error) -// Recursively walk the repo, reading in any .env.example file if present in -// that directory, replacing all `substitutions`, prompting for others, and -// writing to .env.local in that directory. -func InstantiateDotEnv(ctx context.Context, rootDir string, substitutions map[string]string, verbose bool, prompt PromptFunc) error { +// Read .env.example file if present in rootDir, replacing all `substitutions`, +// prompting for others, and returning the result as a map. +func InstantiateDotEnv(ctx context.Context, rootDir string, substitutions map[string]string, verbose bool, prompt PromptFunc) (map[string]string, error) { promptedVars := map[string]string{} - return filepath.WalkDir(rootDir, func(filePath string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - if d.Name() == EnvExampleFile { - envMap, err := godotenv.Read(filePath) - if err != nil { - return err - } + envExamplePath := path.Join(rootDir, EnvExampleFile) + stat, err := os.Stat(envExamplePath) + if err != nil { + return nil, err + } + if stat.IsDir() { + return nil, errors.New("env.example file is a directory") + } - for key, oldValue := range envMap { - // if key is a substitution, replace it - if value, ok := substitutions[key]; ok { - envMap[key] = value - // if key was already promped, use that value - } else if alreadyPromptedValue, ok := promptedVars[key]; ok { - envMap[key] = alreadyPromptedValue - } else { - // prompt for value - newValue, err := prompt(key, oldValue) - if err != nil { - return err - } - envMap[key] = newValue - promptedVars[key] = newValue - } - } + envMap, err := godotenv.Read(envExamplePath) + if err != nil { + return nil, err + } - envContents, err := godotenv.Marshal(envMap) + for key, oldValue := range envMap { + // if key is a substitution, replace it + if value, ok := substitutions[key]; ok { + envMap[key] = value + // if key was already promped, use that value + } else if alreadyPromptedValue, ok := promptedVars[key]; ok { + envMap[key] = alreadyPromptedValue + } else { + // prompt for value + newValue, err := prompt(key, oldValue) if err != nil { - return err - } - - envLocalPath := path.Join(path.Dir(filePath), EnvLocalFile) - if err := os.WriteFile(envLocalPath, []byte(envContents), 0700); err != nil { - return err + return nil, err } + envMap[key] = newValue + promptedVars[key] = newValue } + } + + return envMap, nil +} - return nil - }) +func PrintDotEnv(envMap map[string]string) error { + envContents, err := godotenv.Marshal(envMap) + if err != nil { + return err + } + _, err = fmt.Println(envContents) + return err +} + +func WriteDotEnv(rootDir string, envMap map[string]string) error { + envContents, err := godotenv.Marshal(envMap) + if err != nil { + return err + } + envLocalPath := path.Join(rootDir, EnvLocalFile) + return os.WriteFile(envLocalPath, []byte(envContents), 0700) } func CloneTemplate(url, dir string) (string, string, error) {