Skip to content

Commit 77f1841

Browse files
authored
feat(cmd): better ergonomics around lk app and templates (#474)
* feat(cmd): `lk app env` command just prints to stdout * chore(cmd): make taskfile optional * feat(cmd): support optional path argument for `lk app env`
1 parent be2f016 commit 77f1841

File tree

2 files changed

+89
-49
lines changed

2 files changed

+89
-49
lines changed

cmd/lk/app.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,32 @@ var (
9494
Action: runTask,
9595
},
9696
{
97-
Hidden: true,
98-
Name: "env",
99-
Usage: "Manage environment variables",
100-
Before: requireProject,
97+
Name: "env",
98+
Usage: "Print project environment variables expanded from a .env.example file",
99+
Flags: []cli.Flag{
100+
&cli.BoolFlag{
101+
Name: "w",
102+
Aliases: []string{"write"},
103+
Usage: "Write environment variables to .env.local file",
104+
},
105+
},
106+
ArgsUsage: "[DIR] location of the project directory (default: current directory)",
107+
Before: requireProject,
101108
Action: func(ctx context.Context, cmd *cli.Command) error {
102-
return instantiateEnv(ctx, cmd, ".", nil)
109+
rootDir := cmd.Args().First()
110+
if rootDir == "" {
111+
rootDir = "."
112+
}
113+
114+
env, err := instantiateEnv(ctx, cmd, rootDir, nil)
115+
if err != nil {
116+
return err
117+
}
118+
if cmd.Bool("write") {
119+
return bootstrap.WriteDotEnv(rootDir, env)
120+
} else {
121+
return bootstrap.PrintDotEnv(env)
122+
}
103123
},
104124
},
105125
},
@@ -254,9 +274,11 @@ func setupTemplate(ctx context.Context, cmd *cli.Command) error {
254274

255275
fmt.Println("Instantiating environment...")
256276
addlEnv := &map[string]string{"LIVEKIT_SANDBOX_ID": sandboxID}
257-
if err := instantiateEnv(ctx, cmd, appName, addlEnv); err != nil {
277+
env, err := instantiateEnv(ctx, cmd, appName, addlEnv)
278+
if err != nil {
258279
return err
259280
}
281+
bootstrap.WriteDotEnv(appName, env)
260282

261283
if install {
262284
fmt.Println("Installing template...")
@@ -307,7 +329,7 @@ func cleanupTemplate(ctx context.Context, cmd *cli.Command, appName string) erro
307329
return bootstrap.CleanupTemplate(appName)
308330
}
309331

310-
func instantiateEnv(ctx context.Context, cmd *cli.Command, rootPath string, addlEnv *map[string]string) error {
332+
func instantiateEnv(ctx context.Context, cmd *cli.Command, rootPath string, addlEnv *map[string]string) (map[string]string, error) {
311333
env := map[string]string{
312334
"LIVEKIT_API_KEY": project.APIKey,
313335
"LIVEKIT_API_SECRET": project.APISecret,
@@ -350,6 +372,9 @@ func doPostCreate(ctx context.Context, _ *cli.Command, rootPath string, verbose
350372
if err != nil {
351373
return err
352374
}
375+
if tf == nil {
376+
return nil
377+
}
353378

354379
task, err := bootstrap.NewTask(ctx, tf, rootPath, string(bootstrap.TaskPostCreate), verbose)
355380
if task == nil || err != nil {

pkg/bootstrap/bootstrap.go

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ import (
2121
"context"
2222
"encoding/json"
2323
"errors"
24+
"fmt"
2425
"io"
2526
"io/fs"
2627
"net/http"
2728
"os"
2829
"os/exec"
2930
"path"
30-
"path/filepath"
3131
"runtime"
3232
"strings"
3333

@@ -122,7 +122,14 @@ func FetchSandboxDetails(ctx context.Context, sid, token, serverURL string) (*Sa
122122
}
123123

124124
func ParseTaskfile(rootPath string) (*ast.Taskfile, error) {
125-
file, err := os.ReadFile(path.Join(rootPath, TaskFile))
125+
taskfilePath := path.Join(rootPath, TaskFile)
126+
127+
// taskfile.yaml is optional
128+
if _, err := os.Stat(taskfilePath); err != nil && errors.Is(err, fs.ErrNotExist) {
129+
return nil, nil
130+
}
131+
132+
file, err := os.ReadFile(taskfilePath)
126133
if err != nil {
127134
return nil, err
128135
}
@@ -180,54 +187,62 @@ func NewTask(ctx context.Context, tf *ast.Taskfile, dir, taskName string, verbos
180187

181188
type PromptFunc func(key string, value string) (string, error)
182189

183-
// Recursively walk the repo, reading in any .env.example file if present in
184-
// that directory, replacing all `substitutions`, prompting for others, and
185-
// writing to .env.local in that directory.
186-
func InstantiateDotEnv(ctx context.Context, rootDir string, substitutions map[string]string, verbose bool, prompt PromptFunc) error {
190+
// Read .env.example file if present in rootDir, replacing all `substitutions`,
191+
// prompting for others, and returning the result as a map.
192+
func InstantiateDotEnv(ctx context.Context, rootDir string, substitutions map[string]string, verbose bool, prompt PromptFunc) (map[string]string, error) {
187193
promptedVars := map[string]string{}
188194

189-
return filepath.WalkDir(rootDir, func(filePath string, d fs.DirEntry, err error) error {
190-
if err != nil {
191-
return err
192-
}
193-
194-
if d.Name() == EnvExampleFile {
195-
envMap, err := godotenv.Read(filePath)
196-
if err != nil {
197-
return err
198-
}
195+
envExamplePath := path.Join(rootDir, EnvExampleFile)
196+
stat, err := os.Stat(envExamplePath)
197+
if err != nil {
198+
return nil, err
199+
}
200+
if stat.IsDir() {
201+
return nil, errors.New("env.example file is a directory")
202+
}
199203

200-
for key, oldValue := range envMap {
201-
// if key is a substitution, replace it
202-
if value, ok := substitutions[key]; ok {
203-
envMap[key] = value
204-
// if key was already promped, use that value
205-
} else if alreadyPromptedValue, ok := promptedVars[key]; ok {
206-
envMap[key] = alreadyPromptedValue
207-
} else {
208-
// prompt for value
209-
newValue, err := prompt(key, oldValue)
210-
if err != nil {
211-
return err
212-
}
213-
envMap[key] = newValue
214-
promptedVars[key] = newValue
215-
}
216-
}
204+
envMap, err := godotenv.Read(envExamplePath)
205+
if err != nil {
206+
return nil, err
207+
}
217208

218-
envContents, err := godotenv.Marshal(envMap)
209+
for key, oldValue := range envMap {
210+
// if key is a substitution, replace it
211+
if value, ok := substitutions[key]; ok {
212+
envMap[key] = value
213+
// if key was already promped, use that value
214+
} else if alreadyPromptedValue, ok := promptedVars[key]; ok {
215+
envMap[key] = alreadyPromptedValue
216+
} else {
217+
// prompt for value
218+
newValue, err := prompt(key, oldValue)
219219
if err != nil {
220-
return err
221-
}
222-
223-
envLocalPath := path.Join(path.Dir(filePath), EnvLocalFile)
224-
if err := os.WriteFile(envLocalPath, []byte(envContents), 0700); err != nil {
225-
return err
220+
return nil, err
226221
}
222+
envMap[key] = newValue
223+
promptedVars[key] = newValue
227224
}
225+
}
226+
227+
return envMap, nil
228+
}
228229

229-
return nil
230-
})
230+
func PrintDotEnv(envMap map[string]string) error {
231+
envContents, err := godotenv.Marshal(envMap)
232+
if err != nil {
233+
return err
234+
}
235+
_, err = fmt.Println(envContents)
236+
return err
237+
}
238+
239+
func WriteDotEnv(rootDir string, envMap map[string]string) error {
240+
envContents, err := godotenv.Marshal(envMap)
241+
if err != nil {
242+
return err
243+
}
244+
envLocalPath := path.Join(rootDir, EnvLocalFile)
245+
return os.WriteFile(envLocalPath, []byte(envContents), 0700)
231246
}
232247

233248
func CloneTemplate(url, dir string) (string, string, error) {

0 commit comments

Comments
 (0)