Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ title: Changelog

## [Unreleased](https://github.com/lets-cli/lets/releases/tag/v0.0.X)

* `[Added]` Add `lets self config path` and `lets self config edit` for user settings. Issue [#370](https://github.com/lets-cli/lets/issues/370)
* `[Added]` Show interactive download progress for remote configs and remote mixins. Issue [#360](https://github.com/lets-cli/lets/issues/360)
* `[Fixed]` Make `--no-cache` re-download remote mixins for local and remote configs. Issue [#365](https://github.com/lets-cli/lets/issues/365)
* `[Added]` Remote config support: `lets -c https://url` downloads and caches config to `~/.config/lets/remote-configs/`. Use `--no-cache` to force re-download.
Expand Down
12 changes: 12 additions & 0 deletions docs/docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ Use settings for things like colored output, theming, or update notifications. D

This file is per-user and applies to all projects on the machine.

Print the path:

```bash
lets self config path
```

Open the file in `$EDITOR`:

```bash
lets self config edit
```

## Precedence

Settings are resolved in this order:
Expand Down
70 changes: 67 additions & 3 deletions internal/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bytes"
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -218,7 +220,7 @@ func TestSelfCmd(t *testing.T) {
t.Run("should use help func when run without args", func(t *testing.T) {
rootCmd := CreateRootCommand("v0.0.0-test", "")
rootCmd.SetArgs([]string{"self"})
initSelfCmd(rootCmd, "v0.0.0-test", func(string) error { return nil })
initSelfCmd(rootCmd, "v0.0.0-test", func(string) error { return nil }, func(string) error { return nil })

called := false
rootCmd.SetHelpFunc(func(c *cobra.Command, args []string) {
Expand All @@ -236,6 +238,68 @@ func TestSelfCmd(t *testing.T) {
}
})

t.Run("should print user config path", func(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
bufOut := new(bytes.Buffer)

rootCmd := CreateRootCommand("v0.0.0-test", "")
rootCmd.SetArgs([]string{"self", "config", "path"})
rootCmd.SetOut(bufOut)
rootCmd.SetErr(bufOut)
initSelfCmd(rootCmd, "v0.0.0-test", func(string) error { return nil }, func(string) error { return nil })

if err := rootCmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}

expected := filepath.Join(home, ".config", "lets", "config.yaml") + "\n"
if bufOut.String() != expected {
t.Fatalf("expected %q, got %q", expected, bufOut.String())
}
})

t.Run("should open user config in editor", func(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
bufOut := new(bytes.Buffer)
called := false
gotPath := ""

rootCmd := CreateRootCommand("v0.0.0-test", "")
rootCmd.SetArgs([]string{"self", "config", "edit"})
rootCmd.SetOut(bufOut)
rootCmd.SetErr(bufOut)
initSelfCmd(rootCmd, "v0.0.0-test", func(string) error { return nil }, func(path string) error {
called = true
gotPath = path

return nil
})

if err := rootCmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}

expected := filepath.Join(home, ".config", "lets", "config.yaml")
if !called {
t.Fatal("expected editor to be called")
}

if gotPath != expected {
t.Fatalf("expected editor path %q, got %q", expected, gotPath)
}

info, err := os.Stat(filepath.Dir(expected))
if err != nil {
t.Fatalf("expected config directory to exist: %v", err)
}

if perm := info.Mode().Perm(); perm != 0o700 {
t.Fatalf("expected config directory permissions 0700, got %o", perm)
}
})

t.Run("should open documentation in browser", func(t *testing.T) {
bufOut := new(bytes.Buffer)
called := false
Expand All @@ -252,7 +316,7 @@ func TestSelfCmd(t *testing.T) {
rootCmd.SetArgs([]string{"self", "doc"})
rootCmd.SetOut(bufOut)
rootCmd.SetErr(bufOut)
initSelfCmd(rootCmd, "v0.0.0-test", openURL)
initSelfCmd(rootCmd, "v0.0.0-test", openURL, func(string) error { return nil })

err := rootCmd.Execute()
if err != nil {
Expand All @@ -279,7 +343,7 @@ func TestSelfCmd(t *testing.T) {
rootCmd.SetArgs([]string{"self", "doc"})
rootCmd.SetOut(bufOut)
rootCmd.SetErr(bufOut)
initSelfCmd(rootCmd, "v0.0.0-test", openURL)
initSelfCmd(rootCmd, "v0.0.0-test", openURL, func(string) error { return nil })

err := rootCmd.Execute()
if err == nil {
Expand Down
7 changes: 5 additions & 2 deletions internal/cmd/self.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import (

// InitSelfCmd intializes root 'self' subcommand.
func InitSelfCmd(rootCmd *cobra.Command, version string) {
initSelfCmd(rootCmd, version, util.OpenURL)
initSelfCmd(rootCmd, version, util.OpenURL, util.OpenEditor)
}

func initSelfCmd(rootCmd *cobra.Command, version string, openURL func(string) error) {
func initSelfCmd(
rootCmd *cobra.Command, version string, openURL func(string) error, openEditor func(string) error,
) {
selfCmd := &cobra.Command{
Use: "self",
Hidden: false,
Expand All @@ -24,6 +26,7 @@ func initSelfCmd(rootCmd *cobra.Command, version string, openURL func(string) er

rootCmd.AddCommand(selfCmd)

selfCmd.AddCommand(initConfigCommand(openEditor))
selfCmd.AddCommand(initDocCommand(openURL))
selfCmd.AddCommand(initLspCommand(version))
selfCmd.AddCommand(initSkillsCommand())
Expand Down
73 changes: 73 additions & 0 deletions internal/cmd/self_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package cmd

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/lets-cli/lets/internal/util"
"github.com/spf13/cobra"
)

func initConfigCommand(openEditor func(string) error) *cobra.Command {
configCmd := &cobra.Command{
Use: "config <command>",
Short: "Manage lets user config",
Long: strings.TrimSpace(`Manage the per-user lets settings file.

The user config is stored at ~/.config/lets/config.yaml and applies to all
projects on the machine.`),
Args: validateCommandArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}

configCmd.AddCommand(initConfigPathCommand())
configCmd.AddCommand(initConfigEditCommand(openEditor))

return configCmd
}

func initConfigPathCommand() *cobra.Command {
return &cobra.Command{
Use: "path",
Short: "Print lets user config path",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
path, err := util.LetsUserFile("config.yaml")
if err != nil {
return err
}

_, err = fmt.Fprintln(cmd.OutOrStdout(), path)

return err
},
}
}

func initConfigEditCommand(openEditor func(string) error) *cobra.Command {
return &cobra.Command{
Use: "edit",
Short: "Open lets user config in EDITOR",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
path, err := util.LetsUserFile("config.yaml")
if err != nil {
return err
}

if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return fmt.Errorf("creating config directory: %w", err)
}

if err := openEditor(path); err != nil {
return fmt.Errorf("can not open config: %w", err)
}

return nil
},
}
}
26 changes: 26 additions & 0 deletions internal/util/editor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package util

import (
"errors"
"fmt"
"os"
"os/exec"
)

func OpenEditor(path string) error {
editor := os.Getenv("EDITOR")
if editor == "" {
return errors.New("EDITOR is not set")
}

cmd := exec.Command(editor, path) //nolint:gosec
Comment on lines +10 to +16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): EDITOR values containing arguments (e.g. "vim -u ...") will not work with exec.Command as used here.

Many users configure EDITOR with flags or spaces in the path (e.g. code --wait, vim -u ~/.vimrc). exec.Command(editor, path) treats the entire EDITOR value as the binary name and doesn’t split arguments, so these setups will fail. Consider either parsing EDITOR into binary + args and appending path, or invoking via a shell (e.g. exec.Command("sh", "-c", editor+" "+shellQuote(path))).

cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
return fmt.Errorf("run %s: %w", editor, err)
}

return nil
}
Loading