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 cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func main() {
commands.CodexCommand,
commands.SchemaCommand,
commands.GrepCommand,
commands.ConfigCommand,
}
err = app.Run(os.Args)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion commands/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import (
"os"
"time"

"github.com/malamtime/cli/stloader"
"github.com/gookit/color"
"github.com/invopop/jsonschema"
"github.com/malamtime/cli/model"
"github.com/malamtime/cli/stloader"
"github.com/pkg/browser"
"github.com/urfave/cli/v2"
"go.opentelemetry.io/otel/trace"
Expand Down
11 changes: 11 additions & 0 deletions commands/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package commands

import "github.com/urfave/cli/v2"

var ConfigCommand *cli.Command = &cli.Command{
Name: "config",
Usage: "manage shelltime configuration",
Subcommands: []*cli.Command{
ConfigViewCommand,
},
}
167 changes: 167 additions & 0 deletions commands/config_view.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package commands

import (
"encoding/json"
"fmt"
"os"
"reflect"
"strings"

"github.com/gookit/color"
"github.com/malamtime/cli/model"
"github.com/olekukonko/tablewriter"
"github.com/urfave/cli/v2"
"go.opentelemetry.io/otel/trace"
)

var ConfigViewCommand *cli.Command = &cli.Command{
Name: "view",
Usage: "view current configuration",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "format",
Aliases: []string{"f"},
Value: "table",
Usage: "output format (table/json)",
},
},
Action: configView,
OnUsageError: func(cCtx *cli.Context, err error, isSubcommand bool) error {
color.Red.Println(err.Error())
return nil
},
}

func configView(c *cli.Context) error {
ctx, span := commandTracer.Start(c.Context, "config.view", trace.WithSpanKind(trace.SpanKindClient))
defer span.End()

SetupLogger(os.ExpandEnv("$HOME/" + model.COMMAND_BASE_STORAGE_FOLDER))
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.

medium

Hardcoding the path separator / and using os.ExpandEnv("$HOME") is not platform-independent. According to the repository's general rules, you should use os.UserHomeDir() to get the user's home directory and filepath.Join to construct paths. This will ensure your code works correctly across different operating systems like Windows.

Here's a suggested implementation:

homeDir, err := os.UserHomeDir()
if err != nil {
	return fmt.Errorf("could not determine home directory: %w", err)
}
logPath := filepath.Join(homeDir, model.COMMAND_BASE_STORAGE_FOLDER)
SetupLogger(logPath)

You'll need to import the path/filepath package.

References
  1. For platform-independent paths, use filepath.Join to combine segments and os.UserHomeDir() to get the home directory, rather than hardcoding path separators or environment variables like $HOME.


format := c.String("format")
if format != "table" && format != "json" {
return fmt.Errorf("unsupported format: %s. Use 'table' or 'json'", format)
}

cfg, err := configService.ReadConfigFile(ctx)
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}

if format == "json" {
return outputConfigJSON(cfg)
}
return outputConfigTable(cfg)
}

func outputConfigJSON(cfg model.ShellTimeConfig) error {
jsonData, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonData))
return nil
}

func outputConfigTable(cfg model.ShellTimeConfig) error {
w := tablewriter.NewWriter(os.Stdout)
w.Header([]string{"KEY", "VALUE"})

pairs := flattenConfig(cfg, "")
for _, pair := range pairs {
w.Append([]string{pair.key, pair.value})
}

w.Render()
return nil
}

type keyValuePair struct {
key string
value string
}

func flattenConfig(v interface{}, prefix string) []keyValuePair {
var pairs []keyValuePair

val := reflect.ValueOf(v)
if val.Kind() == reflect.Ptr {
if val.IsNil() {
return pairs
}
val = val.Elem()
}

if val.Kind() != reflect.Struct {
return pairs
}

typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)

// Get JSON tag for the key name
jsonTag := fieldType.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
// Parse the tag to get just the name part
tagParts := strings.Split(jsonTag, ",")
keyName := tagParts[0]

fullKey := keyName
if prefix != "" {
fullKey = prefix + "." + keyName
}

// Handle pointer types
if field.Kind() == reflect.Ptr {
if field.IsNil() {
pairs = append(pairs, keyValuePair{key: fullKey, value: "<not set>"})
continue
}
field = field.Elem()
}

switch field.Kind() {
case reflect.Struct:
// Recursively flatten nested structs
nestedPairs := flattenConfig(field.Interface(), fullKey)
pairs = append(pairs, nestedPairs...)
case reflect.Slice:
if field.Len() == 0 {
pairs = append(pairs, keyValuePair{key: fullKey, value: "[]"})
} else {
// Marshal slice to JSON for display
jsonBytes, err := json.Marshal(field.Interface())
if err != nil {
pairs = append(pairs, keyValuePair{key: fullKey, value: fmt.Sprintf("<%d items>", field.Len())})
} else {
pairs = append(pairs, keyValuePair{key: fullKey, value: string(jsonBytes)})
}
}
case reflect.Bool:
pairs = append(pairs, keyValuePair{key: fullKey, value: fmt.Sprintf("%v", field.Bool())})
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
pairs = append(pairs, keyValuePair{key: fullKey, value: fmt.Sprintf("%d", field.Int())})
case reflect.String:
value := field.String()
if value == "" {
value = "<empty>"
} else if strings.Contains(fullKey, "token") || strings.Contains(strings.ToLower(fullKey), "token") {
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.

medium

The first condition strings.Contains(fullKey, "token") is redundant. The second condition strings.Contains(strings.ToLower(fullKey), "token") already performs a case-insensitive check for "token", which covers the first case as well. You can simplify this line by removing the redundant check.

Suggested change
} else if strings.Contains(fullKey, "token") || strings.Contains(strings.ToLower(fullKey), "token") {
} else if strings.Contains(strings.ToLower(fullKey), "token") {

// Mask sensitive fields
if len(value) > 8 {
value = value[:4] + "****" + value[len(value)-4:]
} else {
value = "****"
}
}
pairs = append(pairs, keyValuePair{key: fullKey, value: value})
default:
pairs = append(pairs, keyValuePair{key: fullKey, value: fmt.Sprintf("%v", field.Interface())})
}
}

return pairs
}
2 changes: 1 addition & 1 deletion commands/dotfiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ var DotfilesCommand *cli.Command = &cli.Command{
OnUsageError: func(cCtx *cli.Context, err error, isSubcommand bool) error {
return nil
},
}
}
2 changes: 1 addition & 1 deletion commands/grep.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"strconv"
"time"

"github.com/malamtime/cli/stloader"
"github.com/gookit/color"
"github.com/malamtime/cli/model"
"github.com/malamtime/cli/stloader"
"github.com/olekukonko/tablewriter"
"github.com/urfave/cli/v2"
"go.opentelemetry.io/otel/trace"
Expand Down
2 changes: 1 addition & 1 deletion commands/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import (
"runtime"
"strings"

"github.com/malamtime/cli/stloader"
"github.com/gookit/color"
"github.com/malamtime/cli/model"
"github.com/malamtime/cli/stloader"
"github.com/urfave/cli/v2"
)

Expand Down
6 changes: 4 additions & 2 deletions daemon/socket.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ func (p *SocketHandler) handleStatus(conn net.Conn) {
}

func (p *SocketHandler) handleCCInfo(conn net.Conn, msg SocketMessage) {
slog.Debug("cc_info socket event received")

// Parse time range from payload, default to "today"
timeRange := CCInfoTimeRangeToday
if payload, ok := msg.Payload.(map[string]interface{}); ok {
Expand All @@ -202,9 +204,9 @@ func (p *SocketHandler) handleCCInfo(conn net.Conn, msg SocketMessage) {
}
}

// Notify activity and get cached cost
p.ccInfoTimer.NotifyActivity()
// Get cached cost first (marks range as active), then notify activity (starts timer)
cache := p.ccInfoTimer.GetCachedCost(timeRange)
p.ccInfoTimer.NotifyActivity()

response := CCInfoResponse{
TotalCostUSD: cache.TotalCostUSD,
Expand Down
Loading