diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6d2fe51..d57200a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: - name: 🔵 Setup Go uses: actions/setup-go@v6 with: - go-version: 1.25.x + go-version: 1.26.x - uses: pre-commit/action@v3.0.1 @@ -49,7 +49,7 @@ jobs: - name: 🔵 Setup Go uses: actions/setup-go@v6 with: - go-version: 1.25.x + go-version: 1.26.x - name: ⏬ Install Dependencies run: go get . diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3d43e79..c69b553 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,6 @@ repos: - id: trailing-whitespace - repo: https://github.com/golangci/golangci-lint - rev: v2.8.0 + rev: v2.12.2 hooks: - id: golangci-lint-fmt diff --git a/cmd/info/version.go b/cmd/info/version.go index 5ca3445..f5b6d0f 100644 --- a/cmd/info/version.go +++ b/cmd/info/version.go @@ -1,3 +1,3 @@ package info -var Version = "0.0.54" +var Version = "0.0.55" diff --git a/cmd/log/log_test.go b/cmd/log/log_test.go index b75b7ca..18d6d7a 100644 --- a/cmd/log/log_test.go +++ b/cmd/log/log_test.go @@ -2,30 +2,21 @@ package log import ( "bytes" - "io" + "regexp" "testing" - "github.com/kloudkit/ws-cli/internals/logger" "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" ) -func TestLogCommand(t *testing.T) { - t.Run("WarnInvokesLogWithFlags", func(t *testing.T) { - var gotLevel, gotMsg string - var gotIndent int - var gotStamp bool - called := 0 +func stripAnsi(s string) string { + re := regexp.MustCompile(`\x1b\[[0-9;]*m`) - original := logger.Log - logger.Log = func(w io.Writer, level, message string, indent int, withStamp bool) { - called++ - gotLevel = level - gotMsg = message - gotIndent = indent - gotStamp = withStamp - } - defer func() { logger.Log = original }() + return re.ReplaceAllString(s, "") +} +func TestLogCommand(t *testing.T) { + t.Run("WarnInvokesLogWithFlags", func(t *testing.T) { buffer := new(bytes.Buffer) cmd := LogCmd cmd.SetOut(buffer) @@ -33,68 +24,39 @@ func TestLogCommand(t *testing.T) { err := cmd.Execute() assert.NilError(t, err) - assert.Equal(t, 1, called) - assert.Equal(t, "warn", gotLevel) - assert.Equal(t, "hello", gotMsg) - assert.Equal(t, 2, gotIndent) - assert.Assert(t, gotStamp) + assert.Assert(t, cmp.Regexp( + `^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z] warn - hello\n$`, + stripAnsi(buffer.String()), + )) }) t.Run("InfoUsesPipeWhenFlagged", func(t *testing.T) { - var gotLevel string - var gotIndent int - var gotStamp bool - called := 0 - - original := logger.Pipe - logger.Pipe = func(r io.Reader, w io.Writer, level string, indent int, withStamp bool) { - called++ - gotLevel = level - gotIndent = indent - gotStamp = withStamp - } - defer func() { logger.Pipe = original }() - + buffer := new(bytes.Buffer) cmd := LogCmd cmd.SetIn(bytes.NewBufferString("foo\n")) - cmd.SetOut(new(bytes.Buffer)) + cmd.SetOut(buffer) cmd.SetArgs([]string{"info", "--pipe", "--indent", "1", "--stamp"}) err := cmd.Execute() assert.NilError(t, err) - assert.Equal(t, 1, called) - assert.Equal(t, "info", gotLevel) - assert.Equal(t, 1, gotIndent) - assert.Assert(t, gotStamp) + assert.Assert(t, cmp.Regexp( + `^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z] info - foo\n$`, + stripAnsi(buffer.String()), + )) }) t.Run("StampInvokesLog", func(t *testing.T) { - called := 0 - var gotLevel, gotMsg string - var gotIndent int - var gotStamp bool - - original := logger.Log - logger.Log = func(w io.Writer, level, message string, indent int, withStamp bool) { - called++ - gotLevel = level - gotMsg = message - gotIndent = indent - gotStamp = withStamp - } - defer func() { logger.Log = original }() - + buffer := new(bytes.Buffer) cmd := LogCmd cmd.PersistentFlags().Set("pipe", "false") - cmd.SetOut(new(bytes.Buffer)) + cmd.SetOut(buffer) cmd.SetArgs([]string{"stamp"}) err := cmd.Execute() assert.NilError(t, err) - assert.Equal(t, 1, called) - assert.Equal(t, "", gotLevel) - assert.Equal(t, "", gotMsg) - assert.Equal(t, 0, gotIndent) - assert.Assert(t, gotStamp) + assert.Assert(t, cmp.Regexp( + `^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z] \n$`, + stripAnsi(buffer.String()), + )) }) } diff --git a/cmd/show/ip.go b/cmd/show/ip.go index 453edb6..48174fb 100644 --- a/cmd/show/ip.go +++ b/cmd/show/ip.go @@ -11,53 +11,34 @@ var ipCmd = &cobra.Command{ Short: "Display IP addresses", } -var ipInternalCmd = &cobra.Command{ - Use: "internal", - Short: "Display the internal IP address", - RunE: func(cmd *cobra.Command, args []string) error { - ip, err := net.GetInternalIP() - if err != nil { - return err - } - - raw, _ := cmd.Flags().GetBool("raw") - if styles.OutputRaw(cmd.OutOrStdout(), raw, ip) { - return nil - } - - styles.PrintTitle(cmd.OutOrStdout(), "Internal IP Address") - styles.PrintKeyCode(cmd.OutOrStdout(), "Address", ip) - - return nil - }, -} +func makeIPCmd(use, short, title string, getter func() (string, error)) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + RunE: func(cmd *cobra.Command, args []string) error { + ip, err := getter() + if err != nil { + return err + } + + raw, _ := cmd.Flags().GetBool("raw") + if styles.OutputRaw(cmd.OutOrStdout(), raw, ip) { + return nil + } + + styles.PrintTitle(cmd.OutOrStdout(), title) + styles.PrintKeyCode(cmd.OutOrStdout(), "Address", ip) -var ipNodeCmd = &cobra.Command{ - Use: "node", - Short: "Display the node/host IP address", - RunE: func(cmd *cobra.Command, args []string) error { - ip, err := net.GetNodeIP() - if err != nil { - return err - } - - raw, _ := cmd.Flags().GetBool("raw") - if styles.OutputRaw(cmd.OutOrStdout(), raw, ip) { return nil - } - - styles.PrintTitle(cmd.OutOrStdout(), "Node IP Address") - styles.PrintKeyCode(cmd.OutOrStdout(), "Address", ip) - - return nil - }, + }, + } } func init() { - ipInternalCmd.Flags().Bool("raw", false, "Output raw value without styling") - ipNodeCmd.Flags().Bool("raw", false, "Output raw value without styling") - - ipCmd.AddCommand(ipInternalCmd, ipNodeCmd) + ipCmd.AddCommand( + makeIPCmd("internal", "Display the internal IP address", "Internal IP Address", net.GetInternalIP), + makeIPCmd("node", "Display the node/host IP address", "Node IP Address", net.GetNodeIP), + ) ShowCmd.AddCommand(ipCmd) } diff --git a/cmd/show/ip_test.go b/cmd/show/ip_test.go new file mode 100644 index 0000000..05e4489 --- /dev/null +++ b/cmd/show/ip_test.go @@ -0,0 +1,50 @@ +package show + +import ( + "bytes" + "errors" + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +func _runIPCmd(t *testing.T, title string, getter func() (string, error)) (stdout, stderr string, err error) { + t.Helper() + cmd := makeIPCmd("ip", "Display an IP address", title, getter) + + var outBuf, errBuf bytes.Buffer + cmd.SetOut(&outBuf) + cmd.SetErr(&errBuf) + cmd.SetArgs([]string{}) + err = cmd.Execute() + return outBuf.String(), errBuf.String(), err +} + +func TestShowIP_Internal_RendersValue(t *testing.T) { + getter := func() (string, error) { return "10.0.0.1", nil } + + stdout, _, err := _runIPCmd(t, "Internal IP Address", getter) + assert.NilError(t, err) + plain := _stripANSI(stdout) + assert.Assert(t, strings.Contains(strings.ToUpper(plain), "INTERNAL IP ADDRESS"), "want title, got: %q", plain) + assert.Assert(t, strings.Contains(plain, "10.0.0.1"), "want IP value, got: %q", plain) +} + +func TestShowIP_Node_RendersValue(t *testing.T) { + getter := func() (string, error) { return "192.168.1.5", nil } + + stdout, _, err := _runIPCmd(t, "Node IP Address", getter) + assert.NilError(t, err) + plain := _stripANSI(stdout) + assert.Assert(t, strings.Contains(strings.ToUpper(plain), "NODE IP ADDRESS"), "want title, got: %q", plain) + assert.Assert(t, strings.Contains(plain, "192.168.1.5"), "want IP value, got: %q", plain) +} + +func TestShowIP_GetterError_NonZeroExit(t *testing.T) { + getter := func() (string, error) { return "", errors.New("simulated") } + + _, _, err := _runIPCmd(t, "Internal IP Address", getter) + assert.Assert(t, err != nil, "want non-nil error (maps to non-zero exit at root)") + assert.ErrorContains(t, err, "simulated") +} diff --git a/cmd/show/path.go b/cmd/show/path.go index 5fa49e0..b927674 100644 --- a/cmd/show/path.go +++ b/cmd/show/path.go @@ -61,8 +61,6 @@ var pathVscodeCmd = &cobra.Command{ } func init() { - pathHomeCmd.Flags().Bool("raw", false, "Output raw value without styling") - pathVscodeCmd.Flags().Bool("raw", false, "Output raw value without styling") pathVscodeCmd.Flags().Bool("workspace", false, "Get the workspace settings") pathCmd.AddCommand(pathHomeCmd, pathVscodeCmd) diff --git a/cmd/show/show.go b/cmd/show/show.go index 54d71a4..e61387f 100644 --- a/cmd/show/show.go +++ b/cmd/show/show.go @@ -8,3 +8,7 @@ var ShowCmd = &cobra.Command{ Use: "show", Short: "Display information about the current workspace instance", } + +func init() { + ShowCmd.PersistentFlags().Bool("raw", false, "Output raw value without styling") +} diff --git a/cmd/show/show_test.go b/cmd/show/show_test.go index 16c0924..f9083bd 100644 --- a/cmd/show/show_test.go +++ b/cmd/show/show_test.go @@ -101,6 +101,11 @@ func _runShow(t *testing.T, args ...string) (stdout, stderr string, exit int) { _ = f.Value.Set(f.DefValue) }) + ShowCmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { + f.Changed = false + _ = f.Value.Set(f.DefValue) + }) + var outBuf, errBuf bytes.Buffer ShowCmd.SetOut(&outBuf) ShowCmd.SetErr(&errBuf) @@ -490,6 +495,23 @@ func TestShowEnv_NonSecret_FilePrefix_ErrorPath(t *testing.T) { assert.Assert(t, strings.Contains(stderr, "WS_SERVER_ROOT"), "want runtimeKey in stderr, got: %q", stderr) } +func TestShow_RawFlagInheritedByAllLeaves(t *testing.T) { + cases := []struct{ args []string }{ + {[]string{"path", "home", "--raw"}}, + {[]string{"path", "vscode-settings", "--raw"}}, + {[]string{"ip", "internal", "--raw"}}, + {[]string{"ip", "node", "--raw"}}, + {[]string{"env", "WS_SERVER_ROOT", "--raw"}}, + } + + for _, c := range cases { + _installEnvFixture(t) + _, stderr, exit := _runShow(t, c.args...) + assert.Equal(t, 0, exit, "args=%v stderr=%q", c.args, stderr) + assert.Assert(t, !strings.Contains(stderr, "unknown flag"), "args=%v stderr=%q", c.args, stderr) + } +} + func TestShowEnv_CheckUnchanged(t *testing.T) { _installEnvFixture(t) t.Setenv("WS_SERVER_PORT", "") diff --git a/go.mod b/go.mod index 4439045..0cec8d2 100644 --- a/go.mod +++ b/go.mod @@ -1,36 +1,36 @@ module github.com/kloudkit/ws-cli -go 1.25.8 +go 1.26.2 require ( charm.land/fang/v2 v2.0.1 + charm.land/glamour/v2 v2.0.0 charm.land/lipgloss/v2 v2.0.3 github.com/prometheus/client_golang v1.23.2 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 - golang.org/x/crypto v0.50.0 - golang.org/x/term v0.42.0 + golang.org/x/crypto v0.51.0 + golang.org/x/term v0.43.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.2 ) require ( - charm.land/glamour/v2 v2.0.0 // indirect - github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/alecthomas/chroma/v2 v2.24.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20260422141423-a0f1f21775f7 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260511121909-c840852527f3 // indirect github.com/charmbracelet/x/ansi v0.11.7 // indirect - github.com/charmbracelet/x/exp/charmtone v0.0.0-20260426004601-d5e63ff0b9ca // indirect - github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect + github.com/charmbracelet/x/exp/charmtone v0.0.0-20260517005351-920740d613be // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20260517005351-920740d613be // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dlclark/regexp2 v1.12.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -49,12 +49,12 @@ require ( github.com/prometheus/procfs v0.20.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.7.8 // indirect - github.com/yuin/goldmark-emoji v1.0.5 // indirect + github.com/yuin/goldmark v1.8.2 // indirect + github.com/yuin/goldmark-emoji v1.0.6 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect - golang.org/x/net v0.52.0 // indirect + golang.org/x/net v0.54.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index 9d5efe7..ff8d1b9 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,12 @@ charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U= charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w= charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU= charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM= +github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -16,16 +20,16 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= -github.com/charmbracelet/ultraviolet v0.0.0-20260422141423-a0f1f21775f7 h1:PeRlqWGEoO0apcS62iEgxQhVnFCTOYyQvi2sUTdf6IE= -github.com/charmbracelet/ultraviolet v0.0.0-20260422141423-a0f1f21775f7/go.mod h1:3YdTxlnV/L0bQ3VN8WOSw8doF7LZV/xawUQ4MuAPDvo= +github.com/charmbracelet/ultraviolet v0.0.0-20260511121909-c840852527f3 h1:pxGjlWZFcRQMWAdtjRelpL3Gbu8iYIyuO3Eqbd037Ow= +github.com/charmbracelet/ultraviolet v0.0.0-20260511121909-c840852527f3/go.mod h1:SnKWaPaTnkTNXJgdgdquu66de12V8pW/b/qlTGaF9xg= github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20260426004601-d5e63ff0b9ca h1:/tGUqs2h/DoQZztzFFPDABBOg/UAbfWoJ46JWUazNDs= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20260426004601-d5e63ff0b9ca/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260517005351-920740d613be h1:kR6M0f6TvoLcqUvW1NHjR+g7eCpaGPjVZOg9qFTk5dI= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260517005351-920740d613be/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= -github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= -github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= +github.com/charmbracelet/x/exp/slice v0.0.0-20260517005351-920740d613be h1:O22D2Od8gEsRGTDPKDTRzx2BGrvVcIAJlwBf+1sTeN0= +github.com/charmbracelet/x/exp/slice v0.0.0-20260517005351-920740d613be/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= @@ -40,12 +44,14 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -98,30 +104,29 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= -github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= -github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internals/config/envref.go b/internals/config/envref.go index 5ac0869..595b802 100644 --- a/internals/config/envref.go +++ b/internals/config/envref.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "strings" - "sync" "github.com/kloudkit/ws-cli/internals/env" "gopkg.in/yaml.v3" @@ -37,24 +36,9 @@ func RuntimeKey(group, prop string) string { return "WS_" + strings.ToUpper(group) + "_" + strings.ToUpper(prop) } -var ( - cacheMu sync.Mutex - cachedPath string - cachedVal *EnvReference - cachedErr error -) - func LoadEnvReference() (*EnvReference, error) { - cacheMu.Lock() - defer cacheMu.Unlock() - path := env.String("WS__INTERNAL_ENV_REFERENCE", DefaultEnvReferencePath) - if cachedVal != nil && cachedPath == path { - return cachedVal, cachedErr - } - cachedPath = path - cachedVal, cachedErr = readEnvReference(path) - return cachedVal, cachedErr + return readEnvReference(path) } func readEnvReference(path string) (*EnvReference, error) { diff --git a/internals/config/envref_test.go b/internals/config/envref_test.go index a0ef993..6e73934 100644 --- a/internals/config/envref_test.go +++ b/internals/config/envref_test.go @@ -1,6 +1,8 @@ package config import ( + "os" + "path/filepath" "testing" "gotest.tools/v3/assert" @@ -148,6 +150,44 @@ envs: assert.ErrorContains(t, err, "secret") } +func TestLoadEnvReference_RereadsOverrideEachCall(t *testing.T) { + fixtureA := ` +envs: + server: + properties: + root: + type: string + default: /workspace +` + fixtureB := ` +envs: + metrics: + properties: + port: + type: integer + default: 9100 +` + dir := t.TempDir() + pathA := filepath.Join(dir, "a.yaml") + pathB := filepath.Join(dir, "b.yaml") + assert.NilError(t, os.WriteFile(pathA, []byte(fixtureA), 0o644)) + assert.NilError(t, os.WriteFile(pathB, []byte(fixtureB), 0o644)) + + t.Setenv("WS__INTERNAL_ENV_REFERENCE", pathA) + first, err := LoadEnvReference() + assert.NilError(t, err) + _, hasRoot := first.Properties["WS_SERVER_ROOT"] + assert.Assert(t, hasRoot, "first load should reflect fixture A") + + t.Setenv("WS__INTERNAL_ENV_REFERENCE", pathB) + second, err := LoadEnvReference() + assert.NilError(t, err) + _, hasPort := second.Properties["WS_METRICS_PORT"] + assert.Assert(t, hasPort, "no-cache contract: second load re-reads the re-pointed override") + _, stillHasRoot := second.Properties["WS_SERVER_ROOT"] + assert.Assert(t, !stillHasRoot, "no-cache contract: re-pointed load must not return the first fixture") +} + func TestParse_DeprecatedTombstone_NotRegisteredAsAlias(t *testing.T) { yamlData := ` envs: diff --git a/internals/logger/logger.go b/internals/logger/logger.go index 0a3efe1..83d0ff2 100644 --- a/internals/logger/logger.go +++ b/internals/logger/logger.go @@ -44,7 +44,7 @@ func formatLevel(level string) string { return level } -var Pipe = func(reader io.Reader, writer io.Writer, level string, indent int, withStamp bool) { +func Pipe(reader io.Reader, writer io.Writer, level string, indent int, withStamp bool) { scanner := bufio.NewScanner(reader) for scanner.Scan() { @@ -52,7 +52,7 @@ var Pipe = func(reader io.Reader, writer io.Writer, level string, indent int, wi } } -var Log = func(writer io.Writer, level, message string, indent int, withStamp bool) { +func Log(writer io.Writer, level, message string, indent int, withStamp bool) { var parts []string if withStamp { parts = append(parts, timestamp()) diff --git a/internals/path/support.go b/internals/path/support.go index 8170cfa..266cf53 100644 --- a/internals/path/support.go +++ b/internals/path/support.go @@ -11,13 +11,14 @@ import ( "github.com/kloudkit/ws-cli/internals/env" ) +var slashRunRe = regexp.MustCompile(`/+`) + func AppendSegments(root string, segments ...string) string { if len(segments) != 0 { root += "/" + strings.Join(segments, "/") } - re := regexp.MustCompile(`/+`) - root = re.ReplaceAllString(root, "/") + root = slashRunRe.ReplaceAllString(root, "/") return strings.TrimSuffix(root, "/") } diff --git a/internals/server/server.go b/internals/server/server.go index b512a79..9fbcc38 100644 --- a/internals/server/server.go +++ b/internals/server/server.go @@ -2,9 +2,9 @@ package server import ( "fmt" + "net" "net/http" "strconv" - "strings" "github.com/kloudkit/ws-cli/internals/styles" ) @@ -14,8 +14,12 @@ type Config struct { Bind string } +func formatAddr(c Config) string { + return net.JoinHostPort(c.Bind, strconv.Itoa(c.Port)) +} + func ServeDirectory(config Config, directory string, description string) error { - host := strings.Join([]string{config.Bind, ":", strconv.Itoa(config.Port)}, "") + host := formatAddr(config) handler := http.FileServer(http.Dir(directory)) diff --git a/internals/server/server_test.go b/internals/server/server_test.go index 5927819..3327089 100644 --- a/internals/server/server_test.go +++ b/internals/server/server_test.go @@ -10,6 +10,23 @@ import ( "gotest.tools/v3/assert" ) +func TestFormatAddr(t *testing.T) { + cases := []struct { + bind string + port int + want string + }{ + {"127.0.0.1", 8080, "127.0.0.1:8080"}, + {"::1", 8080, "[::1]:8080"}, + {"", 0, ":0"}, + {"0.0.0.0", 80, "0.0.0.0:80"}, + } + + for _, c := range cases { + assert.Equal(t, c.want, formatAddr(Config{Port: c.port, Bind: c.bind})) + } +} + func TestConfig(t *testing.T) { t.Run("ConfigFields", func(t *testing.T) { config := Config{