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
10 changes: 6 additions & 4 deletions cmd/feature/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@ func installFeatureByName(cmd *cobra.Command, featureName, featuresDir string) e
}

if err := runAnsiblePlaybook(featurePath, vars); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "\n%s\n", styles.ErrorBadge().Render("ERROR"))
fmt.Fprintf(cmd.ErrOrStderr(), "%s\n", styles.Error().Render(err.Error()))
fmt.Fprintln(cmd.ErrOrStderr())
styles.PrintError(cmd.ErrOrStderr(), err.Error())
os.Exit(1)
}

fmt.Fprintf(cmd.OutOrStdout(), "\n%s\n", styles.Success().Render("✓ Feature installed successfully"))
fmt.Fprintf(cmd.OutOrStdout(), " %s %s\n", styles.Key().Render("Feature:"), styles.Value().Render(featureName))
fmt.Fprintln(cmd.OutOrStdout())
styles.PrintSuccessWithDetails(cmd.OutOrStdout(), "Feature installed successfully", [][]string{
{"Feature", featureName},
})

return nil
}
Expand Down
9 changes: 7 additions & 2 deletions cmd/feature/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ var listCmd = &cobra.Command{
}

if len(availableFeatures) == 0 {
fmt.Fprintln(cmd.OutOrStdout(), styles.Warning().Render("No features found"))
fmt.Fprintln(cmd.OutOrStdout(), styles.Warning().Render("No features found"))
return nil
}

fmt.Fprintf(cmd.OutOrStdout(), "%s\n", styles.TitleWithCount("Features available", len(availableFeatures)))
fmt.Fprintf(cmd.OutOrStdout(), "%s\n", styles.TitleWithCount("Features Available", len(availableFeatures)))

maxNameLen := 0
for _, feature := range availableFeatures {
Expand All @@ -45,6 +45,11 @@ var listCmd = &cobra.Command{

fmt.Fprintln(cmd.OutOrStdout(), styles.List(featureItems...))

styles.PrintHints(cmd.OutOrStdout(), [][]string{
{"ws-cli feature install <name>", "Install a feature"},
{"ws-cli feature info <name>", "View feature details"},
})

return nil
},
}
2 changes: 1 addition & 1 deletion cmd/info/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func showEnvironment(writer io.Writer) {
}
}

fmt.Fprintf(writer, "%s\n", styles.TitleWithCount("Workspace variables", len(wsVars)))
fmt.Fprintf(writer, "%s\n", styles.TitleWithCount("Workspace Variables", len(wsVars)))

sort.Slice(wsVars, func(i, j int) bool {
return wsVars[i][0] < wsVars[j][0]
Expand Down
2 changes: 1 addition & 1 deletion cmd/info/uptime.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func showUptime(writer io.Writer) {
started, running, err := readStartup()

if err != nil {
fmt.Fprintf(writer, "%s\n", styles.Warning().Render("⚠ Could not read workspace startup time"))
styles.PrintWarning(writer, "Could not read workspace startup time")
return
}

Expand Down
18 changes: 8 additions & 10 deletions cmd/logs/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,18 @@ func execute(cmd *cobra.Command, args []string) error {
level, _ := cmd.Flags().GetString("level")

if level != "" && level != "info" && level != "warn" && level != "error" && level != "debug" {
fmt.Fprintf(cmd.ErrOrStderr(), "%s\n\n", styles.ErrorBadge().Render("ERROR"))
fmt.Fprintln(cmd.ErrOrStderr(), styles.Error().Render("Invalid log level. Must be one of:"))
fmt.Fprintf(cmd.ErrOrStderr(), " %s %s\n", styles.Code().Render("debug"), styles.Muted().Render("- Debug information"))
fmt.Fprintf(cmd.ErrOrStderr(), " %s %s\n", styles.Code().Render("info"), styles.Muted().Render("- General information"))
fmt.Fprintf(cmd.ErrOrStderr(), " %s %s\n", styles.Code().Render("warn"), styles.Muted().Render("- Warning messages"))
fmt.Fprintf(cmd.ErrOrStderr(), " %s %s\n", styles.Code().Render("error"), styles.Muted().Render("- Error messages only"))
styles.PrintErrorWithOptions(cmd.ErrOrStderr(), "Invalid log level. Must be one of:", [][]string{
{"debug", "Debug information"},
{"info", "General information"},
{"warn", "Warning messages"},
{"error", "Error messages only"},
})
os.Exit(1)
}

reader, err := logger.NewReader(tail, level)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "%s\n\n", styles.ErrorBadge().Render("ERROR"))
fmt.Fprintln(cmd.ErrOrStderr(), styles.Error().Render(fmt.Sprintf("Failed to initialize log reader: %s", err)))
styles.PrintError(cmd.ErrOrStderr(), fmt.Sprintf("Failed to initialize log reader: %s", err))
os.Exit(1)
}

Expand All @@ -45,8 +44,7 @@ func execute(cmd *cobra.Command, args []string) error {
}

if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "%s\n\n", styles.ErrorBadge().Render("ERROR"))
fmt.Fprintln(cmd.ErrOrStderr(), styles.Error().Render(fmt.Sprintf("Error reading logs: %s", err)))
styles.PrintError(cmd.ErrOrStderr(), fmt.Sprintf("Error reading logs: %s", err))
os.Exit(1)
}

Expand Down
14 changes: 6 additions & 8 deletions cmd/secrets/decrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package secrets

import (
"fmt"
"strings"

internalIO "github.com/kloudkit/ws-cli/internals/io"
internalSecrets "github.com/kloudkit/ws-cli/internals/secrets"
Expand Down Expand Up @@ -31,10 +30,7 @@ var decryptCmd = &cobra.Command{
return err
}

input = strings.ReplaceAll(input, "\r", "")
input = strings.ReplaceAll(input, "\n", "")
input = strings.ReplaceAll(input, " ", "")
input = strings.ReplaceAll(input, "\t", "")
input = internalSecrets.NormalizeEncrypted(input)

decrypted, err := internalSecrets.Decrypt(input, masterKey)
if err != nil {
Expand All @@ -45,8 +41,8 @@ var decryptCmd = &cobra.Command{
if raw {
fmt.Fprint(cmd.OutOrStdout(), string(decrypted))
} else {
fmt.Fprintf(cmd.OutOrStdout(), "%s\n", styles.Header().Render("Decrypted Value"))
fmt.Fprintf(cmd.OutOrStdout(), " %s\n", styles.Code().Render(string(decrypted)))
styles.PrintTitle(cmd.OutOrStdout(), "Decrypted Value")
styles.PrintKeyCode(cmd.OutOrStdout(), "Value", string(decrypted))
}
return nil
}
Expand All @@ -56,7 +52,9 @@ var decryptCmd = &cobra.Command{
}

if !raw {
fmt.Fprintln(cmd.OutOrStdout(), styles.Success().Render(fmt.Sprintf("✓ Decrypted value written to %s", outputFile)))
styles.PrintSuccessWithDetailsCode(cmd.OutOrStdout(), "Secret decrypted successfully", [][]string{
{"Output", outputFile},
})
}
return nil
},
Expand Down
8 changes: 5 additions & 3 deletions cmd/secrets/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ var encryptCmd = &cobra.Command{
if raw {
fmt.Fprintln(cmd.OutOrStdout(), encrypted)
} else {
fmt.Fprintf(cmd.OutOrStdout(), "%s\n", styles.Header().Render("Encrypted Value"))
fmt.Fprintf(cmd.OutOrStdout(), " %s\n", styles.Code().Render(encrypted))
styles.PrintTitle(cmd.OutOrStdout(), "Encrypted Value")
styles.PrintKeyCode(cmd.OutOrStdout(), "Value", encrypted)
}
return nil
}
Expand All @@ -53,7 +53,9 @@ var encryptCmd = &cobra.Command{
}

if !raw {
fmt.Fprintln(cmd.OutOrStdout(), styles.Success().Render(fmt.Sprintf("✓ Encrypted value written to %s", outputFile)))
styles.PrintSuccessWithDetailsCode(cmd.OutOrStdout(), "Secret encrypted successfully", [][]string{
{"Output", outputFile},
})
}
return nil
},
Expand Down
6 changes: 3 additions & 3 deletions cmd/secrets/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ var SecretsCmd = &cobra.Command{

func init() {
SecretsCmd.PersistentFlags().String("master", "", "Master key or path to key file")
SecretsCmd.PersistentFlags().String("mode", "", "File permissions (e.g., 0o600, 384) - only when --output is used")
SecretsCmd.PersistentFlags().Bool("force", false, "Overwrite existing files/values")
SecretsCmd.PersistentFlags().String("mode", "", "File permissions (e.g., 0o600, 384), only when --output is used")
SecretsCmd.PersistentFlags().Bool("force", false, "Overwrite existing files")
SecretsCmd.PersistentFlags().Bool("raw", false, "Output without styling")

SecretsCmd.AddCommand(encryptCmd, decryptCmd, generateCmd)
SecretsCmd.AddCommand(encryptCmd, decryptCmd, generateCmd, vaultCmd)
}
86 changes: 86 additions & 0 deletions cmd/secrets/vault.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package secrets

import (
"fmt"
"strings"

internalSecrets "github.com/kloudkit/ws-cli/internals/secrets"
"github.com/kloudkit/ws-cli/internals/styles"
"github.com/spf13/cobra"
)

var vaultCmd = &cobra.Command{
Use: "vault",
Short: "Decrypt a vault spec with encrypted values",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
inputFile, _ := cmd.Flags().GetString("input")
masterKeyFlag, _ := cmd.Flags().GetString("master")
keys, _ := cmd.Flags().GetStringArray("key")
force, _ := cmd.Flags().GetBool("force")
raw, _ := cmd.Flags().GetBool("raw")
stdout, _ := cmd.Flags().GetBool("stdout")
modeOverride, _ := cmd.Flags().GetString("mode")

vaultPath, err := internalSecrets.ResolveVaultPath(inputFile)
if err != nil {
return err
}

masterKey, err := internalSecrets.ResolveMasterKey(masterKeyFlag)
if err != nil {
return err
}

vault, err := internalSecrets.LoadVault(vaultPath)
if err != nil {
return err
}

opts := internalSecrets.ProcessOptions{
MasterKey: masterKey,
Keys: keys,
Stdout: stdout,
Raw: raw,
Force: force,
ModeOverride: modeOverride,
}

results, err := internalSecrets.ProcessVault(vault, opts)
if err != nil {
return err
}

if stdout {
for key, value := range results {
output := internalSecrets.FormatSecretForStdout(key, value, raw)
fmt.Fprint(cmd.OutOrStdout(), output)
}
return nil
}

if !raw {
fmt.Fprintln(cmd.OutOrStdout(), styles.Success().Render("✓ Vault processed successfully"))
for key, dest := range results {
if strings.HasPrefix(dest, "env:") {
envVar := strings.TrimPrefix(dest, "env:")
fmt.Fprintf(cmd.OutOrStdout(), " %s → %s\n",
styles.Code().Render(key),
styles.Muted().Render(fmt.Sprintf("env:%s", envVar)))
} else {
fmt.Fprintf(cmd.OutOrStdout(), " %s → %s\n",
styles.Code().Render(key),
styles.Muted().Render(dest))
}
}
}

return nil
},
}

func init() {
vaultCmd.Flags().String("input", "", "Path to vault file")
vaultCmd.Flags().StringArray("key", []string{}, "Decrypt only specified key")
vaultCmd.Flags().Bool("stdout", false, "Output decrypted values to stdout")
}
5 changes: 2 additions & 3 deletions cmd/show/show_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package show

import (
"bytes"
"os"
"strings"
"testing"

Expand All @@ -18,7 +17,7 @@ func TestPathHome(t *testing.T) {
})

t.Run("WithoutEnv", func(t *testing.T) {
os.Unsetenv(config.EnvServerRoot)
t.Setenv(config.EnvServerRoot, "")

assertOutputContains(t, []string{"path", "home"}, "/workspace")
})
Expand All @@ -32,7 +31,7 @@ func TestPathVscode(t *testing.T) {
})

t.Run("WithoutEnv", func(t *testing.T) {
os.Unsetenv("HOME")
t.Setenv("HOME", "")

assertOutputContains(t, []string{"path", "vscode-settings"}, "/home/kloud/.local/share/workspace/User/settings.json")
})
Expand Down
13 changes: 8 additions & 5 deletions cmd/template/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ func runApply(cmd *cobra.Command, args []string) error {
config, _ := template.GetTemplate(templateName)
sourcePath := template.SupportedTemplates[templateName].SourcePath

fmt.Fprintf(cmd.OutOrStdout(), "%s\n", styles.Success().Render("✓ Template applied successfully"))
fmt.Fprintf(cmd.OutOrStdout(), " %s %s\n", styles.Key().Render("Template:"), styles.Value().Render(templateName))
fmt.Fprintf(cmd.OutOrStdout(), " %s %s\n", styles.Key().Render("Source:"), styles.Code().Render(sourcePath))
fmt.Fprintf(cmd.OutOrStdout(), " %s %s\n", styles.Key().Render("Target:"), styles.Code().Render(fmt.Sprintf("%s/%s", targetPath, config.OutputName)))
fmt.Fprintf(cmd.OutOrStdout(), "\n%s\n", styles.Muted().Render("Use 'ws-cli template show --local "+templateName+"' to view the applied template"))
styles.PrintSuccess(cmd.OutOrStdout(), "Template applied successfully")
styles.PrintKeyValue(cmd.OutOrStdout(), "Template", templateName)
styles.PrintKeyCode(cmd.OutOrStdout(), "Source", sourcePath)
styles.PrintKeyCode(cmd.OutOrStdout(), "Target", fmt.Sprintf("%s/%s", targetPath, config.OutputName))

styles.PrintHints(cmd.OutOrStdout(), [][]string{
{"ws-cli template show --local " + templateName, "View applied template"},
})

return nil
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/template/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func runList(cmd *cobra.Command, args []string) error {
templates := template.SupportedTemplates
names := template.GetTemplateNames()

fmt.Fprintf(cmd.OutOrStdout(), "%s\n", styles.TitleWithCount("Templates available", len(names)))
fmt.Fprintf(cmd.OutOrStdout(), "%s\n", styles.TitleWithCount("Templates Available", len(names)))

listItems := make([]any, len(names))
for i, name := range names {
Expand Down
2 changes: 2 additions & 0 deletions internals/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package config
const (
EnvSecretsKey = "WS_SECRETS_MASTER_KEY"
EnvSecretsKeyFile = "WS_SECRETS_MASTER_KEY_FILE"
EnvSecretsVault = "WS_SECRETS_VAULT"
EnvLoggingDir = "WS_LOGGING_DIR"
EnvLoggingFile = "WS_LOGGING_MAIN_FILE"
EnvServerRoot = "WS_SERVER_ROOT"
EnvFeaturesDir = "WS_FEATURES_DIR"
EnvIPCSocket = "WS__INTERNAL_IPC_SOCKET"

DefaultSecretsKeyPath = "/etc/workspace/master.key"
DefaultEnvFilePath = "~/.zshenv"
DefaultLoggingDir = "/var/log/workspace"
DefaultLoggingFile = "workspace.log"
DefaultServerRoot = "/workspace"
Expand Down
1 change: 1 addition & 0 deletions internals/config/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
func TestConstants(t *testing.T) {
assert.Equal(t, "WS_SECRETS_MASTER_KEY", EnvSecretsKey)
assert.Equal(t, "WS_SECRETS_MASTER_KEY_FILE", EnvSecretsKeyFile)
assert.Equal(t, "WS_SECRETS_VAULT", EnvSecretsVault)
assert.Equal(t, "WS_LOGGING_DIR", EnvLoggingDir)
assert.Equal(t, "WS_LOGGING_MAIN_FILE", EnvLoggingFile)
assert.Equal(t, "WS_SERVER_ROOT", EnvServerRoot)
Expand Down
7 changes: 7 additions & 0 deletions internals/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package env

import (
"os"
"regexp"
"strings"
)

Expand Down Expand Up @@ -43,3 +44,9 @@ func GetAll() map[string]string {

return result
}

func IsValidName(name string) bool {
return regexp.
MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`).
MatchString(name)
}
16 changes: 16 additions & 0 deletions internals/path/support.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package path
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

Expand Down Expand Up @@ -37,6 +38,21 @@ func ResolveConfigPath(configPath string) string {
return GetHomeDirectory(configPath)
}

func Expand(path string) (string, error) {
path = os.ExpandEnv(path)
path = filepath.Clean(path)

if strings.HasPrefix(path, "~") {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get home directory: %w", err)
}
path = filepath.Join(homeDir, path[1:])
}

return path, nil
}

func GetCurrentWorkingDirectory(segments ...string) (string, error) {
cwd, err := os.Getwd()
if err != nil {
Expand Down
Loading