Browse Source

Restructure the fs and add cobra

andrea 3 months ago
parent
commit
b50932124a
64 changed files with 614 additions and 343 deletions
  1. 21 0
      LICENSE
  2. 290 0
      backup/main.go
  3. 1 1
      backup/server_test.go
  4. 0 0
      cmd.bk/backup/list/delegate.go
  5. 0 0
      cmd.bk/backup/list/list.go
  6. 2 2
      cmd.bk/backup/main.go
  7. 1 1
      cmd.bk/backup/participant.go
  8. 3 3
      cmd.bk/backup/session.go
  9. 0 0
      cmd.bk/backup/textinput/textinput.go
  10. 94 0
      cmd/root.go
  11. 33 12
      go.mod
  12. 81 11
      go.sum
  13. 3 0
      go.work
  14. 34 0
      go.work.sum
  15. 23 285
      main.go
  16. 0 0
      pkg/models/answer.go
  17. 0 0
      pkg/models/collection.go
  18. 0 0
      pkg/models/exam.go
  19. 0 0
      pkg/models/filters.go
  20. 0 0
      pkg/models/group.go
  21. 0 0
      pkg/models/group_test.go
  22. 0 0
      pkg/models/meta.go
  23. 0 0
      pkg/models/models_test.go
  24. 0 0
      pkg/models/participant.go
  25. 0 0
      pkg/models/player.go
  26. 0 0
      pkg/models/question.go
  27. 0 0
      pkg/models/quiz.go
  28. 0 0
      pkg/models/response.go
  29. 0 0
      pkg/models/session.go
  30. 0 0
      pkg/models/tag.go
  31. 2 2
      pkg/sessionmanager/score.go
  32. 3 3
      pkg/sessionmanager/sessionmanager.go
  33. 1 1
      pkg/store/exam.go
  34. 0 0
      pkg/store/file/defaults.go
  35. 2 2
      pkg/store/file/exam.go
  36. 2 2
      pkg/store/file/exam_test.go
  37. 1 1
      pkg/store/file/file.go
  38. 0 0
      pkg/store/file/file_test.go
  39. 2 2
      pkg/store/file/participant.go
  40. 1 1
      pkg/store/file/participant_test.go
  41. 2 2
      pkg/store/file/quiz.go
  42. 2 2
      pkg/store/file/quiz_test.go
  43. 2 2
      pkg/store/file/response.go
  44. 2 2
      pkg/store/file/session.go
  45. 0 0
      pkg/store/file/testdata/exams/participants/jack.json
  46. 0 0
      pkg/store/file/testdata/exams/participants/john.json
  47. 0 0
      pkg/store/file/testdata/exams/participants/wendy.json
  48. 0 0
      pkg/store/file/testdata/exams/quizzes/quiz_1.md
  49. 0 0
      pkg/store/file/testdata/exams/quizzes/quiz_2.md
  50. 0 0
      pkg/store/file/testdata/exams/quizzes/quiz_3.md
  51. 0 0
      pkg/store/file/testdata/quizzes/quiz_1.md
  52. 0 0
      pkg/store/file/testdata/quizzes/quiz_2.md
  53. 0 0
      pkg/store/file/testdata/quizzes/quiz_3.md
  54. 0 0
      pkg/store/file/testdata/quizzes/quiz_4.md
  55. 0 0
      pkg/store/file/testdata/quizzes/quiz_5.md
  56. 1 1
      pkg/store/participant.go
  57. 1 1
      pkg/store/participant_test.go
  58. 1 1
      pkg/store/quiz.go
  59. 1 1
      pkg/store/quiz_test.go
  60. 1 1
      pkg/store/response.go
  61. 1 1
      pkg/store/session.go
  62. 0 0
      pkg/store/store.go
  63. 0 0
      pkg/store/store_test.go
  64. 0 0
      pkg/store/testdata/participants.csv

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright © 2024 Andrea Fazzi dev@andreafazzi.eu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 290 - 0
backup/main.go

@@ -0,0 +1,290 @@
+package main
+
+import (
+	"encoding/json"
+	"io"
+	"log/slog"
+	"math/rand"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"text/template"
+	"time"
+
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/store"
+	"git.andreafazzi.eu/andrea/probo/pkg/store/file"
+	"github.com/lmittmann/tint"
+)
+
+var (
+	DefaultAssetDir    = "assets"
+	DefaultDataDir     = "data"
+	DefaultSessionDir  = "sessions"
+	DefaultResponseDir = "responses"
+	DefaultTemplateDir = "templates"
+	DefaultStaticDir   = "static"
+)
+
+type Config struct {
+	SessionDir  string
+	ResponseDir string
+	TemplateDir string
+	StaticDir   string
+}
+
+type ExamTemplateData struct {
+	*models.Exam
+
+	SessionID string
+}
+
+type Server struct {
+	config *Config
+	mux    *http.ServeMux
+
+	sessionFileStore  *file.SessionFileStore
+	responseFileStore *file.ResponseFileStore
+}
+
+func GetDefaultTemplateDir() string {
+	return filepath.Join(DefaultAssetDir, DefaultTemplateDir)
+}
+
+func GetDefaultStaticDir() string {
+	return filepath.Join(DefaultAssetDir, DefaultStaticDir)
+}
+
+func GetDefaultSessionDir() string {
+	return filepath.Join(DefaultDataDir, DefaultSessionDir)
+}
+
+func GetDefaultResponseDir() string {
+	return filepath.Join(DefaultDataDir, DefaultResponseDir)
+}
+
+func NewServer(config *Config) (*Server, error) {
+	_, err := os.Stat(config.SessionDir)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = os.Stat(config.TemplateDir)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = os.Stat(config.StaticDir)
+	if err != nil {
+		return nil, err
+	}
+
+	sStore, err := file.NewSessionFileStore(
+		&file.FileStoreConfig[*models.Session, *store.SessionStore]{
+			FilePathConfig: file.FilePathConfig{Dir: config.SessionDir, FilePrefix: "session", FileSuffix: ".json"},
+			IndexDirFunc:   file.DefaultIndexDirFunc[*models.Session, *store.SessionStore],
+			CreateEntityFunc: func() *models.Session {
+				return &models.Session{}
+			},
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	rStore, err := file.NewResponseFileStore(
+		&file.FileStoreConfig[*models.Response, *store.ResponseStore]{
+			FilePathConfig: file.FilePathConfig{Dir: config.ResponseDir, FilePrefix: "response", FileSuffix: ".json"},
+			IndexDirFunc:   file.DefaultIndexDirFunc[*models.Response, *store.ResponseStore],
+			CreateEntityFunc: func() *models.Response {
+				return &models.Response{}
+			},
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	rStore.FilePathConfig = file.FilePathConfig{
+		Dir:        config.ResponseDir,
+		FilePrefix: "response",
+		FileSuffix: ".json",
+	}
+
+	s := &Server{
+		config,
+		http.NewServeMux(),
+		sStore,
+		rStore,
+	}
+
+	s.mux.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir(config.StaticDir))))
+	s.mux.HandleFunc("/create", s.createExamSessionHandler)
+	s.mux.HandleFunc("/responses/", s.getResponsesHandler)
+	s.mux.HandleFunc("/", s.getExamHandler)
+
+	return s, nil
+}
+
+func NewDefaultServer() (*Server, error) {
+	return NewServer(&Config{
+		SessionDir:  GetDefaultSessionDir(),
+		ResponseDir: GetDefaultResponseDir(),
+		TemplateDir: GetDefaultTemplateDir(),
+		StaticDir:   GetDefaultStaticDir(),
+	})
+}
+
+func (s *Server) getResponsesHandler(w http.ResponseWriter, r *http.Request) {
+	result := make([]*models.Response, 0)
+
+	urlParts := strings.Split(r.URL.Path, "/")
+
+	sessionID := urlParts[2]
+
+	if r.Method == "GET" {
+		session, err := s.sessionFileStore.Read(sessionID)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		for _, exam := range session.Exams {
+			responses := s.responseFileStore.ReadAll()
+			for _, r := range responses {
+				if r.ID == exam.ID {
+					result = append(result, r)
+				}
+			}
+		}
+		err = json.NewEncoder(w).Encode(result)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+	}
+}
+
+func (s *Server) createExamSessionHandler(w http.ResponseWriter, r *http.Request) {
+	session := new(models.Session)
+
+	data, err := io.ReadAll(r.Body)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	err = session.Unmarshal(data)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	memorySession, err := s.sessionFileStore.Create(session)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	err = json.NewEncoder(w).Encode(memorySession)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	slog.Info("Received a new session", "session", memorySession)
+
+}
+
+func (s *Server) getExamHandler(w http.ResponseWriter, r *http.Request) {
+	urlParts := strings.Split(r.URL.Path, "/")
+
+	sessionID := urlParts[1]
+	token := urlParts[2]
+
+	session, err := s.sessionFileStore.Read(sessionID)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	exam := session.Exams[token]
+
+	if r.Method == "GET" {
+		w.Header().Set("Content-Type", "text/html")
+
+		tplData, err := os.ReadFile(filepath.Join(GetDefaultTemplateDir(), "exam.tpl"))
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+
+		}
+		tmpl := template.Must(template.New("exam").Parse(string(tplData)))
+
+		err = tmpl.Execute(w, ExamTemplateData{exam, session.ID})
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+	}
+
+	if r.Method == "POST" {
+		err := r.ParseForm()
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusBadRequest)
+			return
+		}
+
+		response := new(models.Response)
+		response.UniqueIDFunc = func() string {
+			return exam.GetID()
+		}
+
+		response.Questions = make(map[string]string)
+		for qID, values := range r.Form {
+			for _, aID := range values {
+				response.Questions[qID] = aID
+			}
+		}
+
+		_, err = s.responseFileStore.Create(response)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusBadRequest)
+			return
+		}
+
+		w.Write([]byte("<p>Thank you for your response.</p>"))
+		return
+	}
+
+}
+
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	s.mux.ServeHTTP(w, r)
+}
+
+func generateRandomID() string {
+	id := ""
+	for i := 0; i < 6; i++ {
+		id += strconv.Itoa(rand.Intn(9) + 1)
+	}
+	return id
+}
+
+func main() {
+	slog.SetDefault(slog.New(
+		tint.NewHandler(os.Stdout, &tint.Options{
+			Level:      slog.LevelInfo,
+			TimeFormat: time.Kitchen,
+		}),
+	))
+
+	server, err := NewDefaultServer()
+	if err != nil {
+		panic(err)
+	}
+
+	slog.Info("Probo server started.")
+	http.ListenAndServe(":8080", server)
+}

+ 1 - 1
server_test.go → backup/server_test.go

@@ -12,7 +12,7 @@ import (
 	"testing"
 	"time"
 
-	"git.andreafazzi.eu/andrea/probo/lib/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
 	"github.com/lmittmann/tint"
 	"github.com/remogatto/prettytest"
 )

+ 0 - 0
cmd/probo-cli/list/delegate.go → cmd.bk/backup/list/delegate.go


+ 0 - 0
cmd/probo-cli/list/list.go → cmd.bk/backup/list/list.go


+ 2 - 2
cmd/probo-cli/main.go → cmd.bk/backup/main.go

@@ -6,9 +6,9 @@ import (
 	"os"
 	"time"
 
-	"git.andreafazzi.eu/andrea/probo/lib/store/file"
+	"git.andreafazzi.eu/andrea/probo/pkg/store/file"
 	"github.com/lmittmann/tint"
-	"github.com/urfave/cli/v2"
+	"github.com/urfave/cli"
 )
 
 func main() {

+ 1 - 1
cmd/probo-cli/participant.go → cmd.bk/backup/participant.go

@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"log/slog"
 
-	"git.andreafazzi.eu/andrea/probo/lib/store/file"
+	"git.andreafazzi.eu/andrea/probo/pkg/store/file"
 	"github.com/urfave/cli/v2"
 )
 

+ 3 - 3
cmd/probo-cli/session.go → cmd.bk/backup/session.go

@@ -4,9 +4,9 @@ import (
 	"fmt"
 	"log"
 
-	"git.andreafazzi.eu/andrea/probo/cmd/probo-cli/textinput"
-	"git.andreafazzi.eu/andrea/probo/lib/sessionmanager"
-	"git.andreafazzi.eu/andrea/probo/lib/store/file"
+	"git.andreafazzi.eu/andrea/probo/cmd/textinput"
+	"git.andreafazzi.eu/andrea/probo/pkg/sessionmanager"
+	"git.andreafazzi.eu/andrea/probo/pkg/store/file"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/urfave/cli/v2"
 )

+ 0 - 0
cmd/probo-cli/textinput/textinput.go → cmd.bk/backup/textinput/textinput.go


+ 94 - 0
cmd/root.go

@@ -0,0 +1,94 @@
+/*
+Copyright © 2024 Andrea Fazzi dev@andreafazzi.eu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+var cfgFile string
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "probo",
+	Short: "A brief description of your application",
+	Long: `A longer description that spans multiple lines and likely contains
+examples and usage of using your application. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.`,
+	// Uncomment the following line if your bare application
+	// has an action associated with it:
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	cobra.OnInitialize(initConfig)
+
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+
+	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.probo.yaml)")
+
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}
+
+// initConfig reads in config file and ENV variables if set.
+func initConfig() {
+	if cfgFile != "" {
+		// Use config file from the flag.
+		viper.SetConfigFile(cfgFile)
+	} else {
+		// Find home directory.
+		home, err := os.UserHomeDir()
+		cobra.CheckErr(err)
+
+		// Search config in home directory with name ".probo" (without extension).
+		viper.AddConfigPath(home)
+		viper.SetConfigType("yaml")
+		viper.SetConfigName(".probo")
+	}
+
+	viper.AutomaticEnv() // read in environment variables that match
+
+	// If a config file is found, read it in.
+	if err := viper.ReadInConfig(); err == nil {
+		fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
+	}
+}

+ 33 - 12
go.mod

@@ -1,42 +1,63 @@
 module git.andreafazzi.eu/andrea/probo
 
-go 1.21.5
+go 1.21.6
 
 require (
-	github.com/charmbracelet/bubbles v0.17.1
+	github.com/charmbracelet/bubbles v0.18.0
 	github.com/charmbracelet/bubbletea v0.25.0
 	github.com/charmbracelet/lipgloss v0.9.1
 	github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
-	github.com/google/uuid v1.5.0
-	github.com/lmittmann/tint v1.0.3
+	github.com/google/uuid v1.6.0
+	github.com/lmittmann/tint v1.0.4
 	github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8
-	github.com/urfave/cli/v2 v2.26.0
+	github.com/urfave/cli v1.22.14
+	github.com/urfave/cli/v2 v2.27.1
 	gopkg.in/yaml.v2 v2.4.0
-	gorm.io/gorm v1.25.5
+	gorm.io/gorm v1.25.6
 )
 
 require (
 	github.com/atotto/clipboard v0.1.4 // indirect
 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 	github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
-	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+	github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
+	github.com/fsnotify/fsnotify v1.7.0 // indirect
+	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mattn/go-isatty v0.0.18 // indirect
 	github.com/mattn/go-localereader v0.0.1 // indirect
 	github.com/mattn/go-runewidth v0.0.15 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/muesli/reflow v0.3.0 // indirect
 	github.com/muesli/termenv v0.15.2 // indirect
-	github.com/rivo/uniseg v0.2.0 // indirect
+	github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+	github.com/rivo/uniseg v0.4.6 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
+	github.com/sagikazarmark/locafero v0.4.0 // indirect
+	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
 	github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
+	github.com/sourcegraph/conc v0.3.0 // indirect
+	github.com/spf13/afero v1.11.0 // indirect
+	github.com/spf13/cast v1.6.0 // indirect
+	github.com/spf13/cobra v1.8.0 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/spf13/viper v1.18.2 // indirect
+	github.com/subosito/gotenv v1.6.0 // indirect
 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
-	golang.org/x/sync v0.1.0 // indirect
-	golang.org/x/sys v0.12.0 // indirect
+	go.uber.org/atomic v1.9.0 // indirect
+	go.uber.org/multierr v1.9.0 // indirect
+	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
+	golang.org/x/sync v0.5.0 // indirect
+	golang.org/x/sys v0.15.0 // indirect
 	golang.org/x/term v0.6.0 // indirect
-	golang.org/x/text v0.3.8 // indirect
-	gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 // indirect
+	golang.org/x/text v0.14.0 // indirect
+	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )

+ 81 - 11
go.sum

@@ -1,9 +1,10 @@
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
 github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
-github.com/charmbracelet/bubbles v0.17.1 h1:0SIyjOnkrsfDo88YvPgAWvZMwXe26TP6drRvmkjyUu4=
-github.com/charmbracelet/bubbles v0.17.1/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o=
+github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
+github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
 github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
 github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
 github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
@@ -12,18 +13,34 @@ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2
 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA=
 github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
-github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
-github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
 github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
-github.com/lmittmann/tint v1.0.3 h1:W5PHeA2D8bBJVvabNfQD/XW9HPLZK1XoPZH0cq8NouQ=
-github.com/lmittmann/tint v1.0.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
+github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
 github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
@@ -31,6 +48,8 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
 github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
 github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -39,32 +58,83 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
 github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
 github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
+github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 h1:nRDwTcxV9B3elxMt+1xINX0bwaPdpouqp5fbynexY8U=
 github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8/go.mod h1:jOEnp79oIHy5cvQSHeLcgVJk1GHOOHJHQWps/d1N5Yo=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
+github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
+github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
 github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
 github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
-github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
-github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
+github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
+github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
+github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
+github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
+github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
+github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
+go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
 golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
 golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
-gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A=
+gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=

+ 3 - 0
go.work

@@ -0,0 +1,3 @@
+go 1.21.6
+
+use .

+ 34 - 0
go.work.sum

@@ -0,0 +1,34 @@
+github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=

+ 23 - 285
main.go

@@ -1,290 +1,28 @@
+/*
+Copyright © 2024 Andrea Fazzi dev@andreafazzi.eu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
 package main
 
-import (
-	"encoding/json"
-	"io"
-	"log/slog"
-	"math/rand"
-	"net/http"
-	"os"
-	"path/filepath"
-	"strconv"
-	"strings"
-	"text/template"
-	"time"
-
-	"git.andreafazzi.eu/andrea/probo/lib/models"
-	"git.andreafazzi.eu/andrea/probo/lib/store"
-	"git.andreafazzi.eu/andrea/probo/lib/store/file"
-	"github.com/lmittmann/tint"
-)
-
-var (
-	DefaultAssetDir    = "assets"
-	DefaultDataDir     = "data"
-	DefaultSessionDir  = "sessions"
-	DefaultResponseDir = "responses"
-	DefaultTemplateDir = "templates"
-	DefaultStaticDir   = "static"
-)
-
-type Config struct {
-	SessionDir  string
-	ResponseDir string
-	TemplateDir string
-	StaticDir   string
-}
-
-type ExamTemplateData struct {
-	*models.Exam
-
-	SessionID string
-}
-
-type Server struct {
-	config *Config
-	mux    *http.ServeMux
-
-	sessionFileStore  *file.SessionFileStore
-	responseFileStore *file.ResponseFileStore
-}
-
-func GetDefaultTemplateDir() string {
-	return filepath.Join(DefaultAssetDir, DefaultTemplateDir)
-}
-
-func GetDefaultStaticDir() string {
-	return filepath.Join(DefaultAssetDir, DefaultStaticDir)
-}
-
-func GetDefaultSessionDir() string {
-	return filepath.Join(DefaultDataDir, DefaultSessionDir)
-}
-
-func GetDefaultResponseDir() string {
-	return filepath.Join(DefaultDataDir, DefaultResponseDir)
-}
-
-func NewServer(config *Config) (*Server, error) {
-	_, err := os.Stat(config.SessionDir)
-	if err != nil {
-		return nil, err
-	}
-
-	_, err = os.Stat(config.TemplateDir)
-	if err != nil {
-		return nil, err
-	}
-
-	_, err = os.Stat(config.StaticDir)
-	if err != nil {
-		return nil, err
-	}
-
-	sStore, err := file.NewSessionFileStore(
-		&file.FileStoreConfig[*models.Session, *store.SessionStore]{
-			FilePathConfig: file.FilePathConfig{Dir: config.SessionDir, FilePrefix: "session", FileSuffix: ".json"},
-			IndexDirFunc:   file.DefaultIndexDirFunc[*models.Session, *store.SessionStore],
-			CreateEntityFunc: func() *models.Session {
-				return &models.Session{}
-			},
-		},
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	rStore, err := file.NewResponseFileStore(
-		&file.FileStoreConfig[*models.Response, *store.ResponseStore]{
-			FilePathConfig: file.FilePathConfig{Dir: config.ResponseDir, FilePrefix: "response", FileSuffix: ".json"},
-			IndexDirFunc:   file.DefaultIndexDirFunc[*models.Response, *store.ResponseStore],
-			CreateEntityFunc: func() *models.Response {
-				return &models.Response{}
-			},
-		},
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	rStore.FilePathConfig = file.FilePathConfig{
-		Dir:        config.ResponseDir,
-		FilePrefix: "response",
-		FileSuffix: ".json",
-	}
-
-	s := &Server{
-		config,
-		http.NewServeMux(),
-		sStore,
-		rStore,
-	}
-
-	s.mux.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir(config.StaticDir))))
-	s.mux.HandleFunc("/create", s.createExamSessionHandler)
-	s.mux.HandleFunc("/responses/", s.getResponsesHandler)
-	s.mux.HandleFunc("/", s.getExamHandler)
-
-	return s, nil
-}
-
-func NewDefaultServer() (*Server, error) {
-	return NewServer(&Config{
-		SessionDir:  GetDefaultSessionDir(),
-		ResponseDir: GetDefaultResponseDir(),
-		TemplateDir: GetDefaultTemplateDir(),
-		StaticDir:   GetDefaultStaticDir(),
-	})
-}
-
-func (s *Server) getResponsesHandler(w http.ResponseWriter, r *http.Request) {
-	result := make([]*models.Response, 0)
-
-	urlParts := strings.Split(r.URL.Path, "/")
-
-	sessionID := urlParts[2]
-
-	if r.Method == "GET" {
-		session, err := s.sessionFileStore.Read(sessionID)
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		for _, exam := range session.Exams {
-			responses := s.responseFileStore.ReadAll()
-			for _, r := range responses {
-				if r.ID == exam.ID {
-					result = append(result, r)
-				}
-			}
-		}
-		err = json.NewEncoder(w).Encode(result)
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-	}
-}
-
-func (s *Server) createExamSessionHandler(w http.ResponseWriter, r *http.Request) {
-	session := new(models.Session)
-
-	data, err := io.ReadAll(r.Body)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusBadRequest)
-		return
-	}
-
-	err = session.Unmarshal(data)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusBadRequest)
-		return
-	}
-
-	memorySession, err := s.sessionFileStore.Create(session)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-
-	err = json.NewEncoder(w).Encode(memorySession)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-
-	slog.Info("Received a new session", "session", memorySession)
-
-}
-
-func (s *Server) getExamHandler(w http.ResponseWriter, r *http.Request) {
-	urlParts := strings.Split(r.URL.Path, "/")
-
-	sessionID := urlParts[1]
-	token := urlParts[2]
-
-	session, err := s.sessionFileStore.Read(sessionID)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-
-	exam := session.Exams[token]
-
-	if r.Method == "GET" {
-		w.Header().Set("Content-Type", "text/html")
-
-		tplData, err := os.ReadFile(filepath.Join(GetDefaultTemplateDir(), "exam.tpl"))
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-
-		}
-		tmpl := template.Must(template.New("exam").Parse(string(tplData)))
-
-		err = tmpl.Execute(w, ExamTemplateData{exam, session.ID})
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-	}
-
-	if r.Method == "POST" {
-		err := r.ParseForm()
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusBadRequest)
-			return
-		}
-
-		response := new(models.Response)
-		response.UniqueIDFunc = func() string {
-			return exam.GetID()
-		}
-
-		response.Questions = make(map[string]string)
-		for qID, values := range r.Form {
-			for _, aID := range values {
-				response.Questions[qID] = aID
-			}
-		}
-
-		_, err = s.responseFileStore.Create(response)
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusBadRequest)
-			return
-		}
-
-		w.Write([]byte("<p>Thank you for your response.</p>"))
-		return
-	}
-
-}
-
-func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	s.mux.ServeHTTP(w, r)
-}
-
-func generateRandomID() string {
-	id := ""
-	for i := 0; i < 6; i++ {
-		id += strconv.Itoa(rand.Intn(9) + 1)
-	}
-	return id
-}
+import "git.andreafazzi.eu/andrea/probo/cmd"
 
 func main() {
-	slog.SetDefault(slog.New(
-		tint.NewHandler(os.Stdout, &tint.Options{
-			Level:      slog.LevelInfo,
-			TimeFormat: time.Kitchen,
-		}),
-	))
-
-	server, err := NewDefaultServer()
-	if err != nil {
-		panic(err)
-	}
-
-	slog.Info("Probo server started.")
-	http.ListenAndServe(":8080", server)
+	cmd.Execute()
 }

+ 0 - 0
lib/models/answer.go → pkg/models/answer.go


+ 0 - 0
lib/models/collection.go → pkg/models/collection.go


+ 0 - 0
lib/models/exam.go → pkg/models/exam.go


+ 0 - 0
lib/models/filters.go → pkg/models/filters.go


+ 0 - 0
lib/models/group.go → pkg/models/group.go


+ 0 - 0
lib/models/group_test.go → pkg/models/group_test.go


+ 0 - 0
lib/models/meta.go → pkg/models/meta.go


+ 0 - 0
lib/models/models_test.go → pkg/models/models_test.go


+ 0 - 0
lib/models/participant.go → pkg/models/participant.go


+ 0 - 0
lib/models/player.go → pkg/models/player.go


+ 0 - 0
lib/models/question.go → pkg/models/question.go


+ 0 - 0
lib/models/quiz.go → pkg/models/quiz.go


+ 0 - 0
lib/models/response.go → pkg/models/response.go


+ 0 - 0
lib/models/session.go → pkg/models/session.go


+ 0 - 0
lib/models/tag.go → pkg/models/tag.go


+ 2 - 2
lib/sessionmanager/score.go → pkg/sessionmanager/score.go

@@ -4,8 +4,8 @@ import (
 	"fmt"
 	"log"
 
-	"git.andreafazzi.eu/andrea/probo/lib/models"
-	"git.andreafazzi.eu/andrea/probo/lib/store/file"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/store/file"
 )
 
 type Score struct {

+ 3 - 3
lib/sessionmanager/sessionmanager.go → pkg/sessionmanager/sessionmanager.go

@@ -7,9 +7,9 @@ import (
 	"net/http"
 	"net/url"
 
-	"git.andreafazzi.eu/andrea/probo/lib/models"
-	"git.andreafazzi.eu/andrea/probo/lib/store"
-	"git.andreafazzi.eu/andrea/probo/lib/store/file"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/store"
+	"git.andreafazzi.eu/andrea/probo/pkg/store/file"
 )
 
 type SessionManager struct {

+ 1 - 1
lib/store/exam.go → pkg/store/exam.go

@@ -1,5 +1,5 @@
 package store
 
-import "git.andreafazzi.eu/andrea/probo/lib/models"
+import "git.andreafazzi.eu/andrea/probo/pkg/models"
 
 type ExamStore = Store[*models.Exam]

+ 0 - 0
lib/store/file/defaults.go → pkg/store/file/defaults.go


+ 2 - 2
lib/store/file/exam.go → pkg/store/file/exam.go

@@ -1,8 +1,8 @@
 package file
 
 import (
-	"git.andreafazzi.eu/andrea/probo/lib/models"
-	"git.andreafazzi.eu/andrea/probo/lib/store"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/store"
 )
 
 type ExamFileStore = FileStore[*models.Exam, *store.Store[*models.Exam]]

+ 2 - 2
lib/store/file/exam_test.go → pkg/store/file/exam_test.go

@@ -6,8 +6,8 @@ import (
 	"io"
 	"os"
 
-	"git.andreafazzi.eu/andrea/probo/lib/models"
-	"git.andreafazzi.eu/andrea/probo/lib/store"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/store"
 	"github.com/remogatto/prettytest"
 )
 

+ 1 - 1
lib/store/file/file.go → pkg/store/file/file.go

@@ -9,7 +9,7 @@ import (
 	"strings"
 	"sync"
 
-	"git.andreafazzi.eu/andrea/probo/lib/store"
+	"git.andreafazzi.eu/andrea/probo/pkg/store"
 )
 
 type IndexDirFunc[T FileStorable, K Storer[T]] func(s *FileStore[T, K]) error

+ 0 - 0
lib/store/file/file_test.go → pkg/store/file/file_test.go


+ 2 - 2
lib/store/file/participant.go → pkg/store/file/participant.go

@@ -1,8 +1,8 @@
 package file
 
 import (
-	"git.andreafazzi.eu/andrea/probo/lib/models"
-	"git.andreafazzi.eu/andrea/probo/lib/store"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/store"
 )
 
 type ParticipantFileStore struct {

+ 1 - 1
lib/store/file/participant_test.go → pkg/store/file/participant_test.go

@@ -3,7 +3,7 @@ package file
 import (
 	"os"
 
-	"git.andreafazzi.eu/andrea/probo/lib/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
 	"github.com/remogatto/prettytest"
 )
 

+ 2 - 2
lib/store/file/quiz.go → pkg/store/file/quiz.go

@@ -11,8 +11,8 @@ import (
 	"strings"
 	"time"
 
-	"git.andreafazzi.eu/andrea/probo/lib/models"
-	"git.andreafazzi.eu/andrea/probo/lib/store"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/store"
 	"gopkg.in/yaml.v2"
 )
 

+ 2 - 2
lib/store/file/quiz_test.go → pkg/store/file/quiz_test.go

@@ -5,8 +5,8 @@ import (
 	"os"
 	"path/filepath"
 
-	"git.andreafazzi.eu/andrea/probo/lib/models"
-	"git.andreafazzi.eu/andrea/probo/lib/store"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/store"
 	"github.com/remogatto/prettytest"
 )
 

+ 2 - 2
lib/store/file/response.go → pkg/store/file/response.go

@@ -1,8 +1,8 @@
 package file
 
 import (
-	"git.andreafazzi.eu/andrea/probo/lib/models"
-	"git.andreafazzi.eu/andrea/probo/lib/store"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/store"
 )
 
 type ResponseFileStore = FileStore[*models.Response, *store.Store[*models.Response]]

+ 2 - 2
lib/store/file/session.go → pkg/store/file/session.go

@@ -1,8 +1,8 @@
 package file
 
 import (
-	"git.andreafazzi.eu/andrea/probo/lib/models"
-	"git.andreafazzi.eu/andrea/probo/lib/store"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/store"
 )
 
 type SessionFileStore = FileStore[*models.Session, *store.Store[*models.Session]]

+ 0 - 0
lib/store/file/testdata/exams/participants/jack.json → pkg/store/file/testdata/exams/participants/jack.json


+ 0 - 0
lib/store/file/testdata/exams/participants/john.json → pkg/store/file/testdata/exams/participants/john.json


+ 0 - 0
lib/store/file/testdata/exams/participants/wendy.json → pkg/store/file/testdata/exams/participants/wendy.json


+ 0 - 0
lib/store/file/testdata/exams/quizzes/quiz_1.md → pkg/store/file/testdata/exams/quizzes/quiz_1.md


+ 0 - 0
lib/store/file/testdata/exams/quizzes/quiz_2.md → pkg/store/file/testdata/exams/quizzes/quiz_2.md


+ 0 - 0
lib/store/file/testdata/exams/quizzes/quiz_3.md → pkg/store/file/testdata/exams/quizzes/quiz_3.md


+ 0 - 0
lib/store/file/testdata/quizzes/quiz_1.md → pkg/store/file/testdata/quizzes/quiz_1.md


+ 0 - 0
lib/store/file/testdata/quizzes/quiz_2.md → pkg/store/file/testdata/quizzes/quiz_2.md


+ 0 - 0
lib/store/file/testdata/quizzes/quiz_3.md → pkg/store/file/testdata/quizzes/quiz_3.md


+ 0 - 0
lib/store/file/testdata/quizzes/quiz_4.md → pkg/store/file/testdata/quizzes/quiz_4.md


+ 0 - 0
lib/store/file/testdata/quizzes/quiz_5.md → pkg/store/file/testdata/quizzes/quiz_5.md


+ 1 - 1
lib/store/participant.go → pkg/store/participant.go

@@ -5,7 +5,7 @@ import (
 	"os"
 	"strconv"
 
-	"git.andreafazzi.eu/andrea/probo/lib/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
 	"github.com/gocarina/gocsv"
 )
 

+ 1 - 1
lib/store/participant_test.go → pkg/store/participant_test.go

@@ -1,7 +1,7 @@
 package store
 
 import (
-	"git.andreafazzi.eu/andrea/probo/lib/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
 	"github.com/remogatto/prettytest"
 )
 

+ 1 - 1
lib/store/quiz.go → pkg/store/quiz.go

@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"strings"
 
-	"git.andreafazzi.eu/andrea/probo/lib/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
 )
 
 type ErrQuizAlreadyPresent struct {

+ 1 - 1
lib/store/quiz_test.go → pkg/store/quiz_test.go

@@ -3,7 +3,7 @@ package store
 import (
 	"reflect"
 
-	"git.andreafazzi.eu/andrea/probo/lib/models"
+	"git.andreafazzi.eu/andrea/probo/pkg/models"
 	"github.com/remogatto/prettytest"
 )
 

+ 1 - 1
lib/store/response.go → pkg/store/response.go

@@ -1,5 +1,5 @@
 package store
 
-import "git.andreafazzi.eu/andrea/probo/lib/models"
+import "git.andreafazzi.eu/andrea/probo/pkg/models"
 
 type ResponseStore = Store[*models.Response]

+ 1 - 1
lib/store/session.go → pkg/store/session.go

@@ -1,5 +1,5 @@
 package store
 
-import "git.andreafazzi.eu/andrea/probo/lib/models"
+import "git.andreafazzi.eu/andrea/probo/pkg/models"
 
 type SessionStore = Store[*models.Session]

+ 0 - 0
lib/store/store.go → pkg/store/store.go


+ 0 - 0
lib/store/store_test.go → pkg/store/store_test.go


+ 0 - 0
lib/store/testdata/participants.csv → pkg/store/testdata/participants.csv