From 2915c2fb165926d40ab88d1bb20a1b3670b22358 Mon Sep 17 00:00:00 2001 From: Alex Borodin Date: Mon, 27 Nov 2023 18:53:45 +0100 Subject: [PATCH 1/2] feat: add basic remote handling --- cmd/remote.go | 36 +++++++++ cmd/remote_add.go | 53 ++++++++++++ cmd/remote_remove.go | 26 ++++++ cmd/remote_set_default.go | 26 ++++++ cmd/remote_show.go | 26 ++++++ internal/app/cli/remote.go | 108 +++++++++++++++++++++++++ internal/config/config.go | 18 +++++ internal/remotes/fs.go | 58 ++++++++++++- internal/remotes/fs_test.go | 38 +++++++++ internal/remotes/remote.go | 157 +++++++++++++++++++++++++++++++++--- main.go | 8 +- 11 files changed, 537 insertions(+), 17 deletions(-) create mode 100644 cmd/remote.go create mode 100644 cmd/remote_add.go create mode 100644 cmd/remote_remove.go create mode 100644 cmd/remote_set_default.go create mode 100644 cmd/remote_show.go create mode 100644 internal/app/cli/remote.go create mode 100644 internal/config/config.go diff --git a/cmd/remote.go b/cmd/remote.go new file mode 100644 index 00000000..829d5e4a --- /dev/null +++ b/cmd/remote.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" + "github.com/web-of-things-open-source/tm-catalog-cli/internal/app/cli" +) + +// remoteCmd represents the remote command +var remoteCmd = &cobra.Command{ + Use: "remote", + Short: "Manage remote repositories", + Long: `The command remote and its subcommands allow to manage the list of remote repositories and their settings. +When no subcommand is given, defaults to list.`, + Run: func(cmd *cobra.Command, args []string) { + err := cli.RemoteList() + if err != nil { + os.Exit(1) + } + }, +} + +func init() { + rootCmd.AddCommand(remoteCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // remoteCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // remoteCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/remote_add.go b/cmd/remote_add.go new file mode 100644 index 00000000..a4136e91 --- /dev/null +++ b/cmd/remote_add.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" + "github.com/web-of-things-open-source/tm-catalog-cli/internal/app/cli" +) + +// remoteAddCmd represents the 'remote add' command +var remoteAddCmd = &cobra.Command{ + Use: "add --name --type ( | --file )", + Short: "Add a remote repository", + Long: `Add a remote repository to the tm-catalog configuration file. Depending on the remote type, +the config may be a simple string, like a URL string, or a json file.`, + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name, err := cmd.Flags().GetString("name") + if err != nil { + cli.Stderrf("internal error: %v", err) + os.Exit(1) + } + typ, err := cmd.Flags().GetString("type") + if err != nil { + cli.Stderrf("internal error: %v", err) + os.Exit(1) + } + + confStr := "" + if len(args) > 0 { + confStr = args[0] + } + + confFile, err := cmd.Flags().GetString("file") + if err != nil { + cli.Stderrf("internal error: %v", err) + os.Exit(1) + } + + err = cli.RemoteAdd(name, typ, confStr, confFile) + if err != nil { + _ = cmd.Usage() + os.Exit(1) + } + }, +} + +func init() { + remoteCmd.AddCommand(remoteAddCmd) + remoteAddCmd.Flags().StringP("name", "n", "", "name of remote to add") + remoteAddCmd.Flags().StringP("type", "t", "", "type of remote to add") + remoteAddCmd.Flags().StringP("file", "f", "", "name of the file to read remote config from") +} diff --git a/cmd/remote_remove.go b/cmd/remote_remove.go new file mode 100644 index 00000000..670a0647 --- /dev/null +++ b/cmd/remote_remove.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" + "github.com/web-of-things-open-source/tm-catalog-cli/internal/app/cli" +) + +// remoteRemoveCmd represents the 'remote remove' command +var remoteRemoveCmd = &cobra.Command{ + Use: "remove ", + Short: "Remove the named remote repository from config", + Long: `Remove the named remote repository from config`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := cli.RemoteRemove(args[0]) + if err != nil { + os.Exit(1) + } + }, +} + +func init() { + remoteCmd.AddCommand(remoteRemoveCmd) +} diff --git a/cmd/remote_set_default.go b/cmd/remote_set_default.go new file mode 100644 index 00000000..452d2db0 --- /dev/null +++ b/cmd/remote_set_default.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" + "github.com/web-of-things-open-source/tm-catalog-cli/internal/app/cli" +) + +// remoteSetDefaultCmd represents the 'remote set-default' command +var remoteSetDefaultCmd = &cobra.Command{ + Use: "set-default ", + Short: "Set named remote repository as default", + Long: `Set named remote repository as default`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := cli.RemoteSetDefault(args[0]) + if err != nil { + os.Exit(1) + } + }, +} + +func init() { + remoteCmd.AddCommand(remoteSetDefaultCmd) +} diff --git a/cmd/remote_show.go b/cmd/remote_show.go new file mode 100644 index 00000000..14032ccd --- /dev/null +++ b/cmd/remote_show.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" + "github.com/web-of-things-open-source/tm-catalog-cli/internal/app/cli" +) + +// remoteShowCmd represents the 'remote show' command +var remoteShowCmd = &cobra.Command{ + Use: "show ", + Short: "Shows settings for the remote ", + Long: `Shows settings for the remote `, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + err := cli.RemoteShow(args[0]) + if err != nil { + os.Exit(1) + } + }, +} + +func init() { + remoteCmd.AddCommand(remoteShowCmd) +} diff --git a/internal/app/cli/remote.go b/internal/app/cli/remote.go new file mode 100644 index 00000000..8d297fc3 --- /dev/null +++ b/internal/app/cli/remote.go @@ -0,0 +1,108 @@ +package cli + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "text/tabwriter" + + "github.com/web-of-things-open-source/tm-catalog-cli/internal" + "github.com/web-of-things-open-source/tm-catalog-cli/internal/remotes" +) + +var ErrInvalidArgs = errors.New("invalid arguments") + +func RemoteList() error { + colWidth := columnWidth() + config, err := remotes.ReadConfig() + if err != nil { + Stderrf("Cannot read remotes config: %v", err) + return err + } + table := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + + _, _ = fmt.Fprintf(table, "NAME\tTYPE\tURL\n") + for name, value := range config { + typ := elideString(fmt.Sprintf("%v", value[remotes.KeyRemoteType]), colWidth) + u := elideString(fmt.Sprintf("%v", value[remotes.KeyRemoteUrl]), colWidth) + _, _ = fmt.Fprintf(table, "%s\t%s\t%s\n", elideString(name, colWidth), typ, u) + } + _ = table.Flush() + return nil +} + +func RemoteAdd(name, typ, confStr, confFile string) error { + if name == "" { + Stderrf("invalid name: %v", name) + return ErrInvalidArgs + } + if !isValidType(typ) { + Stderrf("invalid type: %v. Valid types are: %v", typ, remotes.SupportedTypes) + return ErrInvalidArgs + } + + if confStr != "" && confFile != "" { + Stderrf("specify either or , not both") + return ErrInvalidArgs + } + if confStr == "" && confFile == "" { + Stderrf("must specify either or ") + return ErrInvalidArgs + } + + var bytes []byte + if confFile != "" { + var err error + _, bytes, err = internal.ReadRequiredFile(confFile) + if err != nil { + Stderrf("cannot read file: %v", confFile) + return err + } + } + + return remotes.Add(name, typ, confStr, bytes) +} +func RemoteSetDefault(name string) error { + err := remotes.SetDefault(name) + if err != nil { + Stderrf("%v", err) + } + return err +} + +func RemoteRemove(name string) error { + err := remotes.Remove(name) + if err != nil { + Stderrf("%v", err) + } + return err +} + +func RemoteShow(name string) error { + config, err := remotes.ReadConfig() + if err != nil { + Stderrf("Cannot read remotes config: %v", err) + return err + } + if rc, ok := config[name]; ok { + bytes, err := json.MarshalIndent(rc, "", " ") + if err != nil { + Stderrf("couldn't print config: %v", err) + return err + } + fmt.Println(string(bytes)) + } else { + fmt.Printf("no remote named %s\n", name) + } + return nil +} + +func isValidType(typ string) bool { + for _, t := range remotes.SupportedTypes { + if typ == t { + return true + } + } + return false +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 00000000..4e582431 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,18 @@ +package config + +import ( + "os" + "path/filepath" +) + +var HomeDir string +var DefaultConfigDir string + +func init() { + var err error + HomeDir, err = os.UserHomeDir() + if err != nil { + panic(err) + } + DefaultConfigDir = filepath.Join(HomeDir, ".tm-catalog") +} diff --git a/internal/remotes/fs.go b/internal/remotes/fs.go index 5e3594fa..a8af4859 100644 --- a/internal/remotes/fs.go +++ b/internal/remotes/fs.go @@ -34,7 +34,7 @@ func (e *ErrTMExists) Error() string { var winExtraLeadingSlashRegex = regexp.MustCompile("/[a-zA-Z]:.*") func NewFileRemote(config map[string]any) (*FileRemote, error) { - urlString := config["url"].(string) + urlString := config[KeyRemoteUrl].(string) rootUrl, err := url.Parse(urlString) if err != nil { slog.Default().Error("could not parse root URL for file remote", "url", urlString, "error", err) @@ -198,3 +198,59 @@ func (f *FileRemote) Versions(name string) (model.TOCEntry, error) { return *tocThing, nil } +func createFileRemoteConfig(dirName string, bytes []byte) (map[string]any, error) { + if dirName != "" { + dirName, err := dirNameToExpandedUrl(dirName) + if err != nil { + return nil, err + } + return map[string]any{ + KeyRemoteType: RemoteTypeFile, + KeyRemoteUrl: dirName, + }, nil + } else { + var js any + err := json.Unmarshal(bytes, &js) + if err != nil { + return nil, fmt.Errorf("invalid json config: %w", err) + } + rc, ok := js.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid json config. must be a map") + } + rc[KeyRemoteType] = RemoteTypeFile + u, ok := rc[KeyRemoteUrl] + if !ok { + return nil, fmt.Errorf("invalid json config. must have key \"url\"") + } + us, ok := u.(string) + if !ok { + return nil, fmt.Errorf("invalid json config. url must be a string") + } + ur, err := dirNameToExpandedUrl(us) + if err != nil { + return nil, err + } + rc[KeyRemoteUrl] = ur + return rc, nil + + } +} + +func dirNameToExpandedUrl(dir string) (string, error) { + if !strings.HasPrefix(dir, "file:/") { + if filepath.IsAbs(dir) { + return "file:" + dir, nil + } else { + if !strings.HasPrefix(dir, "~") { + var err error + dir, err = filepath.Abs(dir) + if err != nil { + return "", err + } + } + return "file:/" + dir, nil + } + } + return dir, nil +} diff --git a/internal/remotes/fs_test.go b/internal/remotes/fs_test.go index a11a96cc..3ba06238 100644 --- a/internal/remotes/fs_test.go +++ b/internal/remotes/fs_test.go @@ -74,3 +74,41 @@ func TestNewFileRemote(t *testing.T) { assert.Equal(t, filepath.ToSlash("C:\\Users\\user\\Desktop\\tm-catalog"), filepath.ToSlash(remote.root)) } + +func TestCreateFileRemoteConfig(t *testing.T) { + wd, _ := os.Getwd() + + tests := []struct { + strConf string + fileConf string + expRoot string + expErr bool + }{ + {"../dir/name", "", "file:/" + filepath.Join(filepath.Dir(wd), "/dir/name"), false}, + {"./dir/name", "", "file:/" + filepath.Join(wd, "dir/name"), false}, + {"dir/name", "", "file:/" + filepath.Join(wd, "dir/name"), false}, + {".", "", "file:/" + filepath.Join(wd), false}, + {filepath.Join(wd, "dir/name"), "", "file:" + filepath.Join(wd, "dir/name"), false}, + {"~/dir/name", "", "file:/~/dir/name", false}, + {"", ``, "", true}, + {"", `[]`, "", true}, + {"", `{}`, "", true}, + //{"", `{"url":{}}`, "", true}, + //{"", `{"url":"dir/name"}`, "file:/dir/name", false}, + //{"", `{"url":"/dir/name"}`, "file:/dir/name", false}, + //{"", `{"url":"dir/name", "type":"http"}`, "file:dir/name", false}, + } + + for i, test := range tests { + cf, err := createFileRemoteConfig(test.strConf, []byte(test.fileConf)) + if test.expErr { + assert.Error(t, err, "error expected in test %d for %s %s", i, test.strConf, test.fileConf) + continue + } else { + assert.NoError(t, err, "no error expected in test %d for %s %s", i, test.strConf, test.fileConf) + } + assert.Equalf(t, "file", cf[KeyRemoteType], "in test %d for %s %s", i, test.strConf, test.fileConf) + assert.Equalf(t, test.expRoot, cf[KeyRemoteUrl], "in test %d for %s %s", i, test.strConf, test.fileConf) + + } +} diff --git a/internal/remotes/remote.go b/internal/remotes/remote.go index 188a4a95..5114fa7d 100644 --- a/internal/remotes/remote.go +++ b/internal/remotes/remote.go @@ -1,12 +1,32 @@ package remotes import ( + "errors" "fmt" + "os" + "path/filepath" "github.com/spf13/viper" + "github.com/web-of-things-open-source/tm-catalog-cli/internal/config" "github.com/web-of-things-open-source/tm-catalog-cli/internal/model" ) +const ( + KeyRemotes = "remotes" + KeyRemoteType = "type" + KeyRemoteUrl = "url" + KeyRemoteDefault = "default" + + RemoteTypeFile = "file" +) + +type Config map[string]map[string]any + +var ErrNoDefault = errors.New("no default remote config found") +var ErrRemoteNotFound = errors.New("named remote not found") +var ErrRemoteExists = errors.New("named remote already exists") +var SupportedTypes = []string{RemoteTypeFile} + type Remote interface { // Push writes the Thing Model file into the path under root that corresponds to id. // Returns ErrTMExists if the same file is already stored with a different timestamp @@ -17,29 +37,142 @@ type Remote interface { Versions(name string) (model.TOCEntry, error) } +// Get returns the Remote built from config with the given name +// Empty name returns the default remote func Get(name string) (Remote, error) { - remotesConfig := viper.Get("remotes") + remotes, err := ReadConfig() + if err != nil { + return nil, err + } + rc, ok := remotes[name] + if name == "" { + if len(remotes) == 1 { + for _, v := range remotes { + rc = v + } + } else { + found := false + for _, v := range remotes { + if def, ok := v[KeyRemoteDefault]; ok { + if d, ok := def.(bool); ok && d { + rc = v + found = true + break + } + } + } + if !found { + return nil, ErrNoDefault + } + } + } else { + if !ok { + return nil, ErrRemoteNotFound + } + } + + switch t := rc[KeyRemoteType]; t { + case RemoteTypeFile: + return NewFileRemote(rc) + default: + return nil, fmt.Errorf("unsupported remote type: %v. Supported types are %v", t, SupportedTypes) + } + +} + +func ReadConfig() (Config, error) { + remotesConfig := viper.Get(KeyRemotes) remotes, ok := remotesConfig.(map[string]any) if !ok { return nil, fmt.Errorf("invalid remotes contig") } - rc, ok := remotes[name] - if !ok && name == "" && len(remotes) == 1 { - for _, v := range remotes { - rc = v + cp := map[string]map[string]any{} + for k, v := range remotes { + if cfg, ok := v.(map[string]any); ok { + cp[k] = cfg + } else { + return nil, fmt.Errorf("invalid remote config: %s", k) } } + return cp, nil +} - remoteConfig, ok := rc.(map[string]any) - if !ok { - return nil, fmt.Errorf("invalid config of remote \"%s\"", name) +func SetDefault(name string) error { + conf, err := ReadConfig() + if err != nil { + return err + } + if _, ok := conf[name]; !ok { + return ErrRemoteNotFound + } + for n, rc := range conf { + if n == name { + rc[KeyRemoteDefault] = true + } else { + delete(rc, KeyRemoteDefault) + } + } + return saveConfig(conf) +} +func Remove(name string) error { + conf, err := ReadConfig() + if err != nil { + return err + } + if _, ok := conf[name]; !ok { + return ErrRemoteNotFound + } + delete(conf, name) + return saveConfig(conf) +} + +func Add(name, typ, confStr string, confFile []byte) error { + _, err := Get(name) + if err == nil || !errors.Is(err, ErrRemoteNotFound) { + return ErrRemoteExists } - switch t := remoteConfig["type"]; t { - case "file": - return NewFileRemote(remoteConfig) + var rc map[string]any + switch typ { + case RemoteTypeFile: + rc, err = createFileRemoteConfig(confStr, confFile) + if err != nil { + return err + } default: - return nil, fmt.Errorf("unsupported remote type: %v", t) + return fmt.Errorf("unsupported remote type: %v. Supported types are %v", typ, SupportedTypes) + } + + conf, err := ReadConfig() + if err != nil { + return err + } + + conf[name] = rc + + return saveConfig(conf) +} + +func saveConfig(conf Config) error { + dc := 0 + for _, rc := range conf { + d := rc[KeyRemoteDefault] + if b, ok := d.(bool); ok && b { + dc++ + } + } + if dc > 1 { + return fmt.Errorf("too many default remotes. can accept at most one") } + viper.Set(KeyRemotes, conf) + configFile := viper.ConfigFileUsed() + if configFile == "" { + configFile = filepath.Join(config.DefaultConfigDir, "config.json") + } + err := os.MkdirAll(config.DefaultConfigDir, 0700) + if err != nil { + return err + } + return viper.WriteConfigAs(configFile) } diff --git a/main.go b/main.go index c805e346..d0ff0f6a 100644 --- a/main.go +++ b/main.go @@ -6,10 +6,10 @@ package main import ( "log/slog" "os" - "path/filepath" "github.com/spf13/viper" "github.com/web-of-things-open-source/tm-catalog-cli/cmd" + "github.com/web-of-things-open-source/tm-catalog-cli/internal/config" ) func main() { @@ -32,10 +32,9 @@ func initViper() { viper.SetConfigType("json") viper.SetConfigName("config") - dir, err := os.UserHomeDir() - viper.AddConfigPath(filepath.Join(dir, ".tm-catalog")) + viper.AddConfigPath(config.DefaultConfigDir) viper.AddConfigPath(".") - err = viper.ReadInConfig() + err := viper.ReadInConfig() if err != nil { if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { @@ -45,6 +44,7 @@ func initViper() { } } } + viper.WatchConfig() } func setUpLogger() { logLevel := viper.GetString("logLevel") From c98991d6d082e029fc7bfefc2424f893bcfa5166 Mon Sep 17 00:00:00 2001 From: Alex Borodin Date: Tue, 28 Nov 2023 12:54:10 +0100 Subject: [PATCH 2/2] fixup: add missing remote commands chore: reorg the code --- cmd/create-toc.go | 2 +- cmd/fetch.go | 2 +- cmd/list.go | 2 +- cmd/push.go | 2 +- cmd/{ => remote}/remote.go | 5 +- cmd/{ => remote}/remote_add.go | 24 +++---- cmd/{ => remote}/remote_remove.go | 2 +- cmd/remote/remote_rename.go | 26 ++++++++ cmd/remote/remote_set_config.go | 48 ++++++++++++++ cmd/{ => remote}/remote_set_default.go | 2 +- cmd/{ => remote}/remote_show.go | 2 +- cmd/root.go | 10 +-- cmd/validate.go | 2 +- cmd/versions.go | 2 +- internal/app/cli/remote.go | 76 +++++++++++++++++----- internal/commands/push_test.go | 4 +- internal/remotes/fs.go | 88 ++++++++++---------------- internal/remotes/fs_test.go | 37 +++++------ internal/remotes/remote.go | 50 ++++++++++++++- main.go | 5 +- 20 files changed, 266 insertions(+), 125 deletions(-) rename cmd/{ => remote}/remote.go (89%) rename cmd/{ => remote}/remote_add.go (62%) rename cmd/{ => remote}/remote_remove.go (97%) create mode 100644 cmd/remote/remote_rename.go create mode 100644 cmd/remote/remote_set_config.go rename cmd/{ => remote}/remote_set_default.go (97%) rename cmd/{ => remote}/remote_show.go (97%) diff --git a/cmd/create-toc.go b/cmd/create-toc.go index 06928c8b..13b1baed 100644 --- a/cmd/create-toc.go +++ b/cmd/create-toc.go @@ -17,7 +17,7 @@ var createTOCCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(createTOCCmd) + RootCmd.AddCommand(createTOCCmd) createTOCCmd.Flags().StringP("remote", "r", "", "use named remote instead of default") } diff --git a/cmd/fetch.go b/cmd/fetch.go index 2ca16c2c..b3335a58 100644 --- a/cmd/fetch.go +++ b/cmd/fetch.go @@ -16,7 +16,7 @@ var fetchCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(fetchCmd) + RootCmd.AddCommand(fetchCmd) fetchCmd.Flags().StringP("remote", "r", "", "use named remote instead of default") } diff --git a/cmd/list.go b/cmd/list.go index 3010adf2..8bbee2b0 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -16,7 +16,7 @@ var listCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(listCmd) + RootCmd.AddCommand(listCmd) listCmd.Flags().StringP("remote", "r", "", "use named remote instead of default") } diff --git a/cmd/push.go b/cmd/push.go index ccf6dfbd..7c8f17a4 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -33,7 +33,7 @@ file-or-dirname } func init() { - rootCmd.AddCommand(pushCmd) + RootCmd.AddCommand(pushCmd) pushCmd.Flags().StringP("remote", "r", "", "use named remote instead of default") pushCmd.Flags().StringP("opt-path", "p", "", "append optional path to mandatory target directory structure") pushCmd.Flags().BoolP("opt-tree", "t", false, "use original directory tree as optional path for each file. Has no effect with a single file. Overrides -p") diff --git a/cmd/remote.go b/cmd/remote/remote.go similarity index 89% rename from cmd/remote.go rename to cmd/remote/remote.go index 829d5e4a..4c50a68f 100644 --- a/cmd/remote.go +++ b/cmd/remote/remote.go @@ -1,9 +1,10 @@ -package cmd +package remote import ( "os" "github.com/spf13/cobra" + "github.com/web-of-things-open-source/tm-catalog-cli/cmd" "github.com/web-of-things-open-source/tm-catalog-cli/internal/app/cli" ) @@ -22,7 +23,7 @@ When no subcommand is given, defaults to list.`, } func init() { - rootCmd.AddCommand(remoteCmd) + cmd.RootCmd.AddCommand(remoteCmd) // Here you will define your flags and configuration settings. diff --git a/cmd/remote_add.go b/cmd/remote/remote_add.go similarity index 62% rename from cmd/remote_add.go rename to cmd/remote/remote_add.go index a4136e91..d33f894b 100644 --- a/cmd/remote_add.go +++ b/cmd/remote/remote_add.go @@ -1,4 +1,4 @@ -package cmd +package remote import ( "os" @@ -9,26 +9,23 @@ import ( // remoteAddCmd represents the 'remote add' command var remoteAddCmd = &cobra.Command{ - Use: "add --name --type ( | --file )", + Use: "add [--type ] ( | --file )", Short: "Add a remote repository", - Long: `Add a remote repository to the tm-catalog configuration file. Depending on the remote type, -the config may be a simple string, like a URL string, or a json file.`, - Args: cobra.MaximumNArgs(1), + Long: `Add a remote repository to the tm-catalog-cli configuration file. Depending on the remote type, +the config may be a simple string, like a URL, or a json file. +--type is optional only if --file is used and the type is specified there. +`, + Args: cobra.RangeArgs(1, 2), Run: func(cmd *cobra.Command, args []string) { - name, err := cmd.Flags().GetString("name") - if err != nil { - cli.Stderrf("internal error: %v", err) - os.Exit(1) - } typ, err := cmd.Flags().GetString("type") if err != nil { cli.Stderrf("internal error: %v", err) os.Exit(1) } - + name := args[0] confStr := "" - if len(args) > 0 { - confStr = args[0] + if len(args) > 1 { + confStr = args[1] } confFile, err := cmd.Flags().GetString("file") @@ -47,7 +44,6 @@ the config may be a simple string, like a URL string, or a json file.`, func init() { remoteCmd.AddCommand(remoteAddCmd) - remoteAddCmd.Flags().StringP("name", "n", "", "name of remote to add") remoteAddCmd.Flags().StringP("type", "t", "", "type of remote to add") remoteAddCmd.Flags().StringP("file", "f", "", "name of the file to read remote config from") } diff --git a/cmd/remote_remove.go b/cmd/remote/remote_remove.go similarity index 97% rename from cmd/remote_remove.go rename to cmd/remote/remote_remove.go index 670a0647..9fd2819b 100644 --- a/cmd/remote_remove.go +++ b/cmd/remote/remote_remove.go @@ -1,4 +1,4 @@ -package cmd +package remote import ( "os" diff --git a/cmd/remote/remote_rename.go b/cmd/remote/remote_rename.go new file mode 100644 index 00000000..8b06791c --- /dev/null +++ b/cmd/remote/remote_rename.go @@ -0,0 +1,26 @@ +package remote + +import ( + "os" + + "github.com/spf13/cobra" + "github.com/web-of-things-open-source/tm-catalog-cli/internal/app/cli" +) + +// remoteRenameCmd represents the 'remote show' command +var remoteRenameCmd = &cobra.Command{ + Use: "rename ", + Short: "Renames remote to ", + Long: `Renames remote to `, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + err := cli.RemoteRename(args[0], args[1]) + if err != nil { + os.Exit(1) + } + }, +} + +func init() { + remoteCmd.AddCommand(remoteRenameCmd) +} diff --git a/cmd/remote/remote_set_config.go b/cmd/remote/remote_set_config.go new file mode 100644 index 00000000..bc701b15 --- /dev/null +++ b/cmd/remote/remote_set_config.go @@ -0,0 +1,48 @@ +package remote + +import ( + "os" + + "github.com/spf13/cobra" + "github.com/web-of-things-open-source/tm-catalog-cli/internal/app/cli" +) + +// remoteSetConfigCmd represents the 'remote add' command +var remoteSetConfigCmd = &cobra.Command{ + Use: "set-config [--type ] ( | --file )", + Short: "Set config for a remote repository", + Long: `Overwrite config of a remote repository. Depending on the remote type, +the config may be a simple string, like a URL, or a json file. +--type is optional only if --file is used and the type is specified there.`, + Args: cobra.RangeArgs(1, 2), + Run: func(cmd *cobra.Command, args []string) { + typ, err := cmd.Flags().GetString("type") + if err != nil { + cli.Stderrf("internal error: %v", err) + os.Exit(1) + } + name := args[0] + confStr := "" + if len(args) > 1 { + confStr = args[1] + } + + confFile, err := cmd.Flags().GetString("file") + if err != nil { + cli.Stderrf("internal error: %v", err) + os.Exit(1) + } + + err = cli.RemoteSetConfig(name, typ, confStr, confFile) + if err != nil { + _ = cmd.Usage() + os.Exit(1) + } + }, +} + +func init() { + remoteCmd.AddCommand(remoteSetConfigCmd) + remoteSetConfigCmd.Flags().StringP("type", "t", "", "type of remote to add") + remoteSetConfigCmd.Flags().StringP("file", "f", "", "name of the file to read remote config from") +} diff --git a/cmd/remote_set_default.go b/cmd/remote/remote_set_default.go similarity index 97% rename from cmd/remote_set_default.go rename to cmd/remote/remote_set_default.go index 452d2db0..2594f10d 100644 --- a/cmd/remote_set_default.go +++ b/cmd/remote/remote_set_default.go @@ -1,4 +1,4 @@ -package cmd +package remote import ( "os" diff --git a/cmd/remote_show.go b/cmd/remote/remote_show.go similarity index 97% rename from cmd/remote_show.go rename to cmd/remote/remote_show.go index 14032ccd..70e34f9e 100644 --- a/cmd/remote_show.go +++ b/cmd/remote/remote_show.go @@ -1,4 +1,4 @@ -package cmd +package remote import ( "os" diff --git a/cmd/root.go b/cmd/root.go index e3bb0ebf..5b664cfb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,8 +6,8 @@ import ( "github.com/spf13/cobra" ) -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ Use: "tm-catalog-cli", Short: "A CLI client for TM catalogs", Long: `tm-catalog-cli is a CLI client for contributing to and searching @@ -15,9 +15,9 @@ ThingModel catalogs.`, } // 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. +// This is called by main.main(). It only needs to happen once to the RootCmd. func Execute() { - err := rootCmd.Execute() + err := RootCmd.Execute() if err != nil { os.Exit(1) } @@ -29,5 +29,5 @@ func init() { // Cobra supports persistent flags, which, if defined here, // will be global for your application. - // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.tm-catalog-cli.yaml)") + // RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.tm-catalog-cli.yaml)") } diff --git a/cmd/validate.go b/cmd/validate.go index a2187532..0f5b9420 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -20,5 +20,5 @@ var validateCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(validateCmd) + RootCmd.AddCommand(validateCmd) } diff --git a/cmd/versions.go b/cmd/versions.go index b693db24..d5eab9af 100644 --- a/cmd/versions.go +++ b/cmd/versions.go @@ -16,7 +16,7 @@ var versionsCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(versionsCmd) + RootCmd.AddCommand(versionsCmd) versionsCmd.Flags().StringP("remote", "r", "", "use named remote instead of default") } diff --git a/internal/app/cli/remote.go b/internal/app/cli/remote.go index 8d297fc3..a1c78ec0 100644 --- a/internal/app/cli/remote.go +++ b/internal/app/cli/remote.go @@ -22,10 +22,10 @@ func RemoteList() error { } table := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - _, _ = fmt.Fprintf(table, "NAME\tTYPE\tURL\n") + _, _ = fmt.Fprintf(table, "NAME\tTYPE\tLOCATION\n") for name, value := range config { typ := elideString(fmt.Sprintf("%v", value[remotes.KeyRemoteType]), colWidth) - u := elideString(fmt.Sprintf("%v", value[remotes.KeyRemoteUrl]), colWidth) + u := elideString(fmt.Sprintf("%v", value[remotes.KeyRemoteLoc]), colWidth) _, _ = fmt.Fprintf(table, "%s\t%s\t%s\n", elideString(name, colWidth), typ, u) } _ = table.Flush() @@ -33,35 +33,64 @@ func RemoteList() error { } func RemoteAdd(name, typ, confStr, confFile string) error { - if name == "" { + return remoteSaveConfig(name, typ, confStr, confFile, remotes.Add) +} +func RemoteSetConfig(name, typ, confStr, confFile string) error { + return remoteSaveConfig(name, typ, confStr, confFile, remotes.SetConfig) +} + +func remoteSaveConfig(name, typ, confStr, confFile string, saver func(name, typ, confStr string, confFile []byte) error) error { + if !remotes.ValidRemoteNameRegex.MatchString(name) { Stderrf("invalid name: %v", name) return ErrInvalidArgs } + var bytes []byte + if confFile != "" { + var err error + _, bytes, err = internal.ReadRequiredFile(confFile) + if err != nil { + Stderrf("cannot read file: %v", confFile) + return err + } + } + + typ = inferType(typ, bytes) + if !isValidType(typ) { Stderrf("invalid type: %v. Valid types are: %v", typ, remotes.SupportedTypes) return ErrInvalidArgs } if confStr != "" && confFile != "" { - Stderrf("specify either or , not both") + Stderrf("specify either or --file=, not both") return ErrInvalidArgs } if confStr == "" && confFile == "" { - Stderrf("must specify either or ") + Stderrf("must specify either or --file=") return ErrInvalidArgs } - - var bytes []byte - if confFile != "" { - var err error - _, bytes, err = internal.ReadRequiredFile(confFile) - if err != nil { - Stderrf("cannot read file: %v", confFile) - return err + err := saver(name, typ, confStr, bytes) + if err != nil { + Stderrf("error saving remote config: %v", err) + } + return err +} +func inferType(typ string, bytes []byte) string { + if typ != "" { + return typ + } + if len(bytes) > 0 { + config, err := remotes.AsRemoteConfig(bytes) + if err == nil { + t := config[remotes.KeyRemoteType] + if t != nil { + if ts, ok := t.(string); ok { + return ts + } + } } } - - return remotes.Add(name, typ, confStr, bytes) + return "" } func RemoteSetDefault(name string) error { err := remotes.SetDefault(name) @@ -94,10 +123,27 @@ func RemoteShow(name string) error { fmt.Println(string(bytes)) } else { fmt.Printf("no remote named %s\n", name) + return remotes.ErrRemoteNotFound } return nil } +func RemoteRename(oldName, newName string) (err error) { + err = remotes.Rename(oldName, newName) + if err != nil { + if errors.Is(err, remotes.ErrRemoteNotFound) { + Stderrf("remote %s not found", oldName) + return + } + if errors.Is(err, remotes.ErrInvalidRemoteName) { + Stderrf("invalid remote name: %s", newName) + return + } + Stderrf("error renaming a remote: %v", err) + } + return +} + func isValidType(typ string) bool { for _, t := range remotes.SupportedTypes { if typ == t { diff --git a/internal/commands/push_test.go b/internal/commands/push_test.go index a8ce6873..b8ccd5fa 100644 --- a/internal/commands/push_test.go +++ b/internal/commands/push_test.go @@ -122,7 +122,7 @@ func TestPushToRemoteUnversioned(t *testing.T) { remote, err := remotes.NewFileRemote( map[string]any{ "type": "file", - "url": "file:" + root, + "loc": root, }) assert.NoError(t, err) @@ -175,7 +175,7 @@ func TestPushToRemoteVersioned(t *testing.T) { remote, err := remotes.NewFileRemote( map[string]any{ "type": "file", - "url": "file:" + root, + "loc": root, }) assert.NoError(t, err) diff --git a/internal/remotes/fs.go b/internal/remotes/fs.go index a8af4859..daf83c44 100644 --- a/internal/remotes/fs.go +++ b/internal/remotes/fs.go @@ -5,10 +5,8 @@ import ( "errors" "fmt" "log/slog" - "net/url" "os" "path/filepath" - "regexp" "sort" "strings" @@ -17,7 +15,8 @@ import ( "github.com/web-of-things-open-source/tm-catalog-cli/internal/toc" ) -const defaultFilePermissions = os.ModePerm //fixme: review permissions +const defaultDirPermissions = 0775 +const defaultFilePermissions = 0664 type FileRemote struct { root string @@ -31,35 +30,16 @@ func (e *ErrTMExists) Error() string { return fmt.Sprintf("Thing Model already exists under id: %v", e.ExistingId) } -var winExtraLeadingSlashRegex = regexp.MustCompile("/[a-zA-Z]:.*") - func NewFileRemote(config map[string]any) (*FileRemote, error) { - urlString := config[KeyRemoteUrl].(string) - rootUrl, err := url.Parse(urlString) - if err != nil { - slog.Default().Error("could not parse root URL for file remote", "url", urlString, "error", err) - return nil, fmt.Errorf("could not parse root URL %s for file remote: %w", urlString, err) - } - if rootUrl.Scheme != "file" { - slog.Default().Error("root URL for file remote must begin with 'file:'", "url", urlString) - return nil, fmt.Errorf("root URL for file remote must begin with file: %s", urlString) + loc := config[KeyRemoteLoc] + locString, ok := loc.(string) + if !ok { + return nil, fmt.Errorf("invalid file remote config. loc is either not found or not a string: %v", loc) } - rootPath := rootUrl.Path - if rootPath == "" { - rootPath = rootUrl.Opaque // maybe the user just forgot a slash in the url and it's been parsed as opaque - } - rootPath, err = internal.ExpandHome(rootPath) + rootPath, err := internal.ExpandHome(locString) if err != nil { return nil, err } - //err = os.MkdirAll(rootPath, defaultFilePermissions) - //if err != nil { - // return nil, err - //} - if winExtraLeadingSlashRegex.MatchString(rootPath) { - rootPath = strings.TrimPrefix(rootPath, "/") - } - slog.Default().Info("created FileRemote", "root", rootPath) return &FileRemote{ root: rootPath, }, nil @@ -70,7 +50,7 @@ func (f *FileRemote) Push(id model.TMID, raw []byte) error { return errors.New("nothing to write") } fullPath, dir := f.filenames(id) - err := os.MkdirAll(dir, defaultFilePermissions) + err := os.MkdirAll(dir, defaultDirPermissions) if err != nil { return fmt.Errorf("could not create directory %s: %w", dir, err) } @@ -181,7 +161,7 @@ func (f *FileRemote) Versions(name string) (model.TOCEntry, error) { log := slog.Default() if len(name) == 0 { log.Error("Please specify a name to show the TM.") - return model.TOCEntry{}, errors.New("Please specify a name to show the TM") + return model.TOCEntry{}, errors.New("please specify a name to show the TM") } toc, err := f.List("") if err != nil { @@ -200,57 +180,53 @@ func (f *FileRemote) Versions(name string) (model.TOCEntry, error) { } func createFileRemoteConfig(dirName string, bytes []byte) (map[string]any, error) { if dirName != "" { - dirName, err := dirNameToExpandedUrl(dirName) + absDir, err := makeAbs(dirName) if err != nil { return nil, err } return map[string]any{ KeyRemoteType: RemoteTypeFile, - KeyRemoteUrl: dirName, + KeyRemoteLoc: absDir, }, nil } else { - var js any - err := json.Unmarshal(bytes, &js) + rc, err := AsRemoteConfig(bytes) if err != nil { - return nil, fmt.Errorf("invalid json config: %w", err) + return nil, err } - rc, ok := js.(map[string]any) - if !ok { - return nil, fmt.Errorf("invalid json config. must be a map") + if rType, ok := rc[KeyRemoteType]; ok { + if rType != RemoteTypeFile { + return nil, fmt.Errorf("invalid json config. type must be \"file\" or absent") + } } rc[KeyRemoteType] = RemoteTypeFile - u, ok := rc[KeyRemoteUrl] + l, ok := rc[KeyRemoteLoc] if !ok { - return nil, fmt.Errorf("invalid json config. must have key \"url\"") + return nil, fmt.Errorf("invalid json config. must have key \"loc\"") } - us, ok := u.(string) + ls, ok := l.(string) if !ok { return nil, fmt.Errorf("invalid json config. url must be a string") } - ur, err := dirNameToExpandedUrl(us) + la, err := makeAbs(ls) if err != nil { return nil, err } - rc[KeyRemoteUrl] = ur + rc[KeyRemoteLoc] = la return rc, nil - } } -func dirNameToExpandedUrl(dir string) (string, error) { - if !strings.HasPrefix(dir, "file:/") { - if filepath.IsAbs(dir) { - return "file:" + dir, nil - } else { - if !strings.HasPrefix(dir, "~") { - var err error - dir, err = filepath.Abs(dir) - if err != nil { - return "", err - } +func makeAbs(dir string) (string, error) { + if filepath.IsAbs(dir) { + return dir, nil + } else { + if !strings.HasPrefix(dir, "~") { + var err error + dir, err = filepath.Abs(dir) + if err != nil { + return "", err } - return "file:/" + dir, nil } + return dir, nil } - return dir, nil } diff --git a/internal/remotes/fs_test.go b/internal/remotes/fs_test.go index 3ba06238..11d33fea 100644 --- a/internal/remotes/fs_test.go +++ b/internal/remotes/fs_test.go @@ -13,7 +13,7 @@ func TestNewFileRemote(t *testing.T) { remote, err := NewFileRemote( map[string]any{ "type": "file", - "url": "file:" + root, + "loc": root, }) assert.NoError(t, err) assert.Equal(t, root, remote.root) @@ -22,7 +22,7 @@ func TestNewFileRemote(t *testing.T) { remote, err = NewFileRemote( map[string]any{ "type": "file", - "url": "file://" + root, + "loc": root, }) assert.NoError(t, err) assert.Equal(t, root, remote.root) @@ -31,7 +31,7 @@ func TestNewFileRemote(t *testing.T) { remote, err = NewFileRemote( map[string]any{ "type": "file", - "url": "file:/" + root, + "loc": root, }) assert.NoError(t, err) home, _ := os.UserHomeDir() @@ -41,7 +41,7 @@ func TestNewFileRemote(t *testing.T) { remote, err = NewFileRemote( map[string]any{ "type": "file", - "url": "file:" + root, + "loc": root, }) assert.NoError(t, err) assert.Equal(t, filepath.Join(home, "tm-catalog"), remote.root) @@ -50,7 +50,7 @@ func TestNewFileRemote(t *testing.T) { remote, err = NewFileRemote( map[string]any{ "type": "file", - "url": "file:///" + root, + "loc": root, }) assert.NoError(t, err) assert.Equal(t, filepath.Join(home, "tm-catalog"), remote.root) @@ -59,7 +59,7 @@ func TestNewFileRemote(t *testing.T) { remote, err = NewFileRemote( map[string]any{ "type": "file", - "url": "file:/" + root, + "loc": root, }) assert.NoError(t, err) assert.Equal(t, filepath.ToSlash("c:\\Users\\user\\Desktop\\tm-catalog"), filepath.ToSlash(remote.root)) @@ -68,7 +68,7 @@ func TestNewFileRemote(t *testing.T) { remote, err = NewFileRemote( map[string]any{ "type": "file", - "url": "file:///" + root, + "loc": root, }) assert.NoError(t, err) assert.Equal(t, filepath.ToSlash("C:\\Users\\user\\Desktop\\tm-catalog"), filepath.ToSlash(remote.root)) @@ -84,19 +84,20 @@ func TestCreateFileRemoteConfig(t *testing.T) { expRoot string expErr bool }{ - {"../dir/name", "", "file:/" + filepath.Join(filepath.Dir(wd), "/dir/name"), false}, - {"./dir/name", "", "file:/" + filepath.Join(wd, "dir/name"), false}, - {"dir/name", "", "file:/" + filepath.Join(wd, "dir/name"), false}, - {".", "", "file:/" + filepath.Join(wd), false}, - {filepath.Join(wd, "dir/name"), "", "file:" + filepath.Join(wd, "dir/name"), false}, - {"~/dir/name", "", "file:/~/dir/name", false}, + {"../dir/name", "", filepath.Join(filepath.Dir(wd), "/dir/name"), false}, + {"./dir/name", "", filepath.Join(wd, "dir/name"), false}, + {"dir/name", "", filepath.Join(wd, "dir/name"), false}, + {"/dir/name", "", filepath.Join(filepath.VolumeName(wd), "/dir/name"), false}, + {".", "", filepath.Join(wd), false}, + {filepath.Join(wd, "dir/name"), "", filepath.Join(wd, "dir/name"), false}, + {"~/dir/name", "", "~/dir/name", false}, {"", ``, "", true}, {"", `[]`, "", true}, {"", `{}`, "", true}, - //{"", `{"url":{}}`, "", true}, - //{"", `{"url":"dir/name"}`, "file:/dir/name", false}, - //{"", `{"url":"/dir/name"}`, "file:/dir/name", false}, - //{"", `{"url":"dir/name", "type":"http"}`, "file:dir/name", false}, + {"", `{"loc":{}}`, "", true}, + {"", `{"loc":"dir/name"}`, filepath.Join(wd, "dir/name"), false}, + {"", `{"loc":"/dir/name"}`, filepath.Join(filepath.VolumeName(wd), "/dir/name"), false}, + {"", `{"loc":"dir/name", "type":"http"}`, "", true}, } for i, test := range tests { @@ -108,7 +109,7 @@ func TestCreateFileRemoteConfig(t *testing.T) { assert.NoError(t, err, "no error expected in test %d for %s %s", i, test.strConf, test.fileConf) } assert.Equalf(t, "file", cf[KeyRemoteType], "in test %d for %s %s", i, test.strConf, test.fileConf) - assert.Equalf(t, test.expRoot, cf[KeyRemoteUrl], "in test %d for %s %s", i, test.strConf, test.fileConf) + assert.Equalf(t, test.expRoot, cf[KeyRemoteLoc], "in test %d for %s %s", i, test.strConf, test.fileConf) } } diff --git a/internal/remotes/remote.go b/internal/remotes/remote.go index 5114fa7d..84a3e8a0 100644 --- a/internal/remotes/remote.go +++ b/internal/remotes/remote.go @@ -1,10 +1,12 @@ package remotes import ( + "encoding/json" "errors" "fmt" "os" "path/filepath" + "regexp" "github.com/spf13/viper" "github.com/web-of-things-open-source/tm-catalog-cli/internal/config" @@ -14,16 +16,19 @@ import ( const ( KeyRemotes = "remotes" KeyRemoteType = "type" - KeyRemoteUrl = "url" + KeyRemoteLoc = "loc" KeyRemoteDefault = "default" RemoteTypeFile = "file" ) +var ValidRemoteNameRegex = regexp.MustCompile("^[a-zA-Z0-9][\\w\\-_:]*$") + type Config map[string]map[string]any var ErrNoDefault = errors.New("no default remote config found") var ErrRemoteNotFound = errors.New("named remote not found") +var ErrInvalidRemoteName = errors.New("invalid remote name") var ErrRemoteExists = errors.New("named remote already exists") var SupportedTypes = []string{RemoteTypeFile} @@ -132,6 +137,18 @@ func Add(name, typ, confStr string, confFile []byte) error { return ErrRemoteExists } + return setRemoteConfig(name, typ, confStr, confFile, err) +} +func SetConfig(name, typ, confStr string, confFile []byte) error { + _, err := Get(name) + if err != nil && errors.Is(err, ErrRemoteNotFound) { + return ErrRemoteNotFound + } + + return setRemoteConfig(name, typ, confStr, confFile, err) +} + +func setRemoteConfig(name string, typ string, confStr string, confFile []byte, err error) error { var rc map[string]any switch typ { case RemoteTypeFile: @@ -153,6 +170,22 @@ func Add(name, typ, confStr string, confFile []byte) error { return saveConfig(conf) } +func Rename(oldName, newName string) error { + if !ValidRemoteNameRegex.MatchString(newName) { + return ErrInvalidRemoteName + } + conf, err := ReadConfig() + if err != nil { + return err + } + if rc, ok := conf[oldName]; ok { + conf[newName] = rc + delete(conf, oldName) + return saveConfig(conf) + } else { + return ErrRemoteNotFound + } +} func saveConfig(conf Config) error { dc := 0 for _, rc := range conf { @@ -170,9 +203,22 @@ func saveConfig(conf Config) error { if configFile == "" { configFile = filepath.Join(config.DefaultConfigDir, "config.json") } - err := os.MkdirAll(config.DefaultConfigDir, 0700) + err := os.MkdirAll(config.DefaultConfigDir, 0770) if err != nil { return err } return viper.WriteConfigAs(configFile) } + +func AsRemoteConfig(bytes []byte) (map[string]any, error) { + var js any + err := json.Unmarshal(bytes, &js) + if err != nil { + return nil, fmt.Errorf("invalid json config: %w", err) + } + rc, ok := js.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid json config. must be a map") + } + return rc, nil +} diff --git a/main.go b/main.go index d0ff0f6a..d9fe070f 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/viper" "github.com/web-of-things-open-source/tm-catalog-cli/cmd" + _ "github.com/web-of-things-open-source/tm-catalog-cli/cmd/remote" "github.com/web-of-things-open-source/tm-catalog-cli/internal/config" ) @@ -23,9 +24,9 @@ func init() { func initViper() { viper.SetDefault("remotes", map[string]any{ - "localfs": map[string]any{ + "local": map[string]any{ "type": "file", - "url": "file:~/tm-catalog", + "loc": "~/tm-catalog", }, }) viper.SetDefault("logLevel", "INFO")