Skip to content

Commit

Permalink
"Setup" command (#10)
Browse files Browse the repository at this point in the history
It:
* Exits when done
* Writes new fields as correct primitive
* Allows for both deletion and reconfiguratoin
  • Loading branch information
baalimago authored May 27, 2024
1 parent 09419fb commit ba4263a
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 1 deletion.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ module github.com/baalimago/clai

go 1.22

require github.com/baalimago/go_away_boilerplate v1.3.9
require github.com/baalimago/go_away_boilerplate v1.3.10

require golang.org/x/net v0.24.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
github.com/baalimago/go_away_boilerplate v1.3.9 h1:oLtCiTNZAU2OM528QKxTsXyWXjTuK1j2YYVLXpyN7JQ=
github.com/baalimago/go_away_boilerplate v1.3.9/go.mod h1:2O+zQ0Zm8vPD5SeccFFlgyf3AnYWQSHAut/ecPMmRdU=
github.com/baalimago/go_away_boilerplate v1.3.10 h1:zID0+yZPimRZxw1XM8KcdBQ+IT9fbAqlA8pfYCe1Qrc=
github.com/baalimago/go_away_boilerplate v1.3.10/go.mod h1:2O+zQ0Zm8vPD5SeccFFlgyf3AnYWQSHAut/ecPMmRdU=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
11 changes: 11 additions & 0 deletions internal/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/baalimago/clai/internal/glob"
"github.com/baalimago/clai/internal/models"
"github.com/baalimago/clai/internal/photo"
"github.com/baalimago/clai/internal/setup"
"github.com/baalimago/clai/internal/text"
"github.com/baalimago/clai/internal/utils"
"github.com/baalimago/go_away_boilerplate/pkg/ancli"
Expand All @@ -31,6 +32,7 @@ const (
GLOB
PHOTO
VERSION
SETUP
)

var defaultFlags = Configurations{
Expand Down Expand Up @@ -60,6 +62,8 @@ func getModeFromArgs(cmd string) (Mode, error) {
return GLOB, nil
case "help", "h":
return HELP, nil
case "setup", "s":
return SETUP, nil
case "version", "v":
return VERSION, nil
default:
Expand Down Expand Up @@ -174,6 +178,13 @@ func Setup(usage string) (models.Querier, error) {
}
fmt.Printf("version: %v, go version: %v, checksum: %v\n", bi.Main.Version, bi.GoVersion, bi.Main.Sum)
os.Exit(0)
case SETUP:
err := setup.Run()
if err != nil {
return nil, fmt.Errorf("failed to run setup: %v", err)
}
os.Exit(0)
return nil, nil
default:
return nil, fmt.Errorf("unknown mode: %v", mode)
}
Expand Down
244 changes: 244 additions & 0 deletions internal/setup/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package setup

import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strconv"

"github.com/baalimago/go_away_boilerplate/pkg/ancli"
"github.com/baalimago/go_away_boilerplate/pkg/misc"
)

type config struct {
name string
filePath string
}

type action uint8

const (
unset action = iota
conf
del
)

// Run the setup to configure the different files
func Run() error {
var input string
fmt.Print("Do you wish to configure:\n\t0. mode-files (example: textConfig.json/photoConfig.json)\n\t1. model files (example: openai-gpt-4o.json, anthropic-claude-opus.json)\n[0/1]: ")
fmt.Scanln(&input)
var configs []config
var a action
switch input {
case "0":
t, err := modeConfigs()
if err != nil {
return fmt.Errorf("failed to get config files: %v", err)
}
configs = t
a = conf
case "1":
t, err := modelConfigs()
if err != nil {
return fmt.Errorf("failed to get model configs: %v", err)
}

configs = t
default:
return fmt.Errorf("unrecognized selection: %v", input)
}
return configure(configs, a)
}

// modelConfigs gets the model configuration files using pattern os.UserConfigDir()/.clai/*.json
func modelConfigs() ([]config, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return nil, fmt.Errorf("failed to get user config directory: %v", err)
}

pattern := filepath.Join(configDir, ".clai", "*.json")
files, err := filepath.Glob(pattern)
if err != nil {
return nil, fmt.Errorf("failed to glob pattern %v: %v", pattern, err)
}

var configs []config
for _, file := range files {
if filepath.Base(file) == "textConfig.json" || filepath.Base(file) == "photoConfig.json" {
continue
}
configs = append(configs, config{
name: filepath.Base(file),
filePath: file,
})
}

return configs, nil
}

// modeConfigs gets the mode configuration files using pattern os.UserConfigDir()/.clai/*Config.json
func modeConfigs() ([]config, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return nil, fmt.Errorf("failed to get user config directory: %v", err)
}

pattern := filepath.Join(configDir, ".clai", "*Config.json")
files, err := filepath.Glob(pattern)
if err != nil {
return nil, fmt.Errorf("failed to glob pattern %v: %v", pattern, err)
}

var configs []config
for _, file := range files {
configs = append(configs, config{
name: filepath.Base(file),
filePath: file,
})
}

return configs, nil
}

func configure(cfgs []config, a action) error {
fmt.Println("Found config files: ")
for i, cfg := range cfgs {
fmt.Printf("\t%v: %v\n", i, cfg.name)
}

var input string
fmt.Print("Please pick index: ")
fmt.Scanln(&input)
index, err := strconv.Atoi(input)
if err != nil {
return fmt.Errorf("invalid response, failed to convert choice: %v, to integer: %v", input, err)
}
if index < 0 || index >= len(cfgs) {
return fmt.Errorf("invalid index: %v, must be between 0 and %v", index, len(cfgs))
}
if a == unset {
fmt.Print("Do you wish to [c]onfigure or [d]elete?\n[c/d]: ")
fmt.Scanln(&input)
switch input {
case "c", "configure":
a = conf
case "d", "delete":
a = del
default:
return fmt.Errorf("invalid choice: %v", input)
}
}

switch a {
case conf:
return reconfigure(cfgs[index])
case del:
return remove(cfgs[index])
default:
return fmt.Errorf("invalid action, expected conf or del: %v", input)
}
}

func reconfigure(cfg config) error {
f, err := os.Open(cfg.filePath)
if err != nil {
return fmt.Errorf("failed to open file %s: %v", cfg.filePath, err)
}
b, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("failed to read file %s: %v", cfg.filePath, err)
}
return interractiveReconfigure(cfg, b)
}

func remove(cfg config) error {
fmt.Printf("Are you sure you want to delete: '%v'?\n[y/n]: ", cfg.filePath)
var input string
fmt.Scanln(&input)
if input != "y" {
return fmt.Errorf("aborting deletion")
}
err := os.Remove(cfg.filePath)
if err != nil {
return fmt.Errorf("failed to delete file: '%v', error: %v", cfg.filePath, err)
}
ancli.PrintOK(fmt.Sprintf("deleted file: '%v'\n", cfg.filePath))
return nil
}

func interractiveReconfigure(cfg config, b []byte) error {
var jzon map[string]any
err := json.Unmarshal(b, &jzon)
if err != nil {
return fmt.Errorf("failed to unmarshal config: %v, error: %w", cfg.name, err)
}
fmt.Printf("Current config:\n%s\n---\n", b)
newConfig, err := buildNewConfig(jzon)

Check failure on line 180 in internal/setup/setup.go

View workflow job for this annotation

GitHub Actions / call-workflow / validate

this value of err is never used (SA4006)

newB, err := json.MarshalIndent(newConfig, "", "\t")
if err != nil {
return fmt.Errorf("failed to marshal new config: %v", err)
}
err = os.WriteFile(cfg.filePath, newB, 0644)
if err != nil {
return fmt.Errorf("failed to write new config at: '%v', error: %v", cfg.filePath, err)
}
ancli.PrintOK(fmt.Sprintf("wrote new config to: '%v'\n", cfg.filePath))
return nil
}

func buildNewConfig(jzon map[string]any) (map[string]any, error) {
newConfig := make(map[string]any)
for k, v := range jzon {
var input string
var newValue any
maplike, isMap := v.(map[string]any)
if isMap {
m, err := buildNewConfig(maplike)
if err != nil {
return nil, fmt.Errorf("failed to parse nested map-like: %v", err)
}
newValue = m
} else {
fmt.Printf("Key: '%v', current: '%v'\nPlease enter new value, or leave empty to keep: ", k, v)
fmt.Scanln(&input)
if input == "" {
newValue = v
} else {
newValue = input
newValue = castPrimitive(newValue)
}
}
newConfig[k] = newValue
}
return newConfig, nil
}

func castPrimitive(v any) any {
if misc.Truthy(v) {
return true
}

if misc.Falsy(v) {
return false
}

s, isString := v.(string)
if !isString {
// We don't really know what unholy value this might be, but let's just return it and hope it's benign
return v
}
i, err := strconv.Atoi(s)
if err == nil {
return i
}
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return f
}
return s
}

0 comments on commit ba4263a

Please sign in to comment.