diff --git a/cmd/ext.go b/cmd/ext.go index df88562..9e528d5 100644 --- a/cmd/ext.go +++ b/cmd/ext.go @@ -1,12 +1,32 @@ package cmd import ( + "errors" + "os" + "github.com/lunarway/shuttle/internal/extensions" + "github.com/lunarway/shuttle/internal/global" "github.com/spf13/cobra" ) +type extGlobalConfig struct { + registry string +} + +func (c *extGlobalConfig) Registry() (string, bool) { + if c.registry != "" { + return c.registry, true + } + + if registryEnv := os.Getenv("SHUTTLE_EXTENSIONS_REGISTRY"); registryEnv != "" { + return registryEnv, true + } + + return "", false +} + func newExtCmd() *cobra.Command { - extManager := extensions.NewExtensionsManager("some registry") + globalConfig := &extGlobalConfig{} cmd := &cobra.Command{ Use: "ext", @@ -14,19 +34,27 @@ func newExtCmd() *cobra.Command { } cmd.AddCommand( - newExtInstallCmd(extManager), - newExtUpdateCmd(extManager), - newExtInitCmd(extManager), + newExtInstallCmd(globalConfig), + newExtUpdateCmd(globalConfig), + newExtInitCmd(globalConfig), ) + cmd.PersistentFlags().StringVar(&globalConfig.registry, "registry", "", "the given registry, if not set will default to SHUTTLE_EXTENSIONS_REGISTRY") + return cmd } -func newExtInstallCmd(extManager *extensions.ExtensionsManager) *cobra.Command { +func newExtInstallCmd(globalConfig *extGlobalConfig) *cobra.Command { cmd := &cobra.Command{ - Use: "install", - Long: "Install ensures that extensions already known about are downloaded and available", + Use: "install", + Short: "Install ensures that extensions already known about are downloaded and available", RunE: func(cmd *cobra.Command, args []string) error { + extManager := extensions.NewExtensionsManager(global.NewGlobalStore()) + + if err := extManager.Install(cmd.Context()); err != nil { + return err + } + return nil }, } @@ -34,11 +62,22 @@ func newExtInstallCmd(extManager *extensions.ExtensionsManager) *cobra.Command { return cmd } -func newExtUpdateCmd(extManager *extensions.ExtensionsManager) *cobra.Command { +func newExtUpdateCmd(globalConfig *extGlobalConfig) *cobra.Command { cmd := &cobra.Command{ - Use: "update", - Long: "Update will fetch the latest version of the extensions from the given registry", + Use: "update", + Short: "Update will fetch the latest version of the extensions from the given registry", RunE: func(cmd *cobra.Command, args []string) error { + extManager := extensions.NewExtensionsManager(global.NewGlobalStore()) + + registry, ok := globalConfig.Registry() + if !ok { + return errors.New("registry is not set") + } + + if err := extManager.Update(cmd.Context(), registry); err != nil { + return err + } + return nil }, } @@ -46,10 +85,10 @@ func newExtUpdateCmd(extManager *extensions.ExtensionsManager) *cobra.Command { return cmd } -func newExtInitCmd(extManager *extensions.ExtensionsManager) *cobra.Command { +func newExtInitCmd(globalConfig *extGlobalConfig) *cobra.Command { cmd := &cobra.Command{ - Use: "init", - Long: "init will create an initial extensions repository", + Use: "init", + Short: "init will create an initial extensions repository", RunE: func(cmd *cobra.Command, args []string) error { return nil }, diff --git a/internal/extensions/extension.go b/internal/extensions/extension.go new file mode 100644 index 0000000..4e9dd4b --- /dev/null +++ b/internal/extensions/extension.go @@ -0,0 +1,4 @@ +package extensions + +// Extension is the descriptor of a single extension, it is used to add description to the cli, as well as calling the specific extension in question +type Extension struct{} diff --git a/internal/extensions/extensions.go b/internal/extensions/extensions.go index c4872d5..dc249ff 100644 --- a/internal/extensions/extensions.go +++ b/internal/extensions/extensions.go @@ -1,31 +1,50 @@ package extensions -import "context" +import ( + "context" + "fmt" + "github.com/lunarway/shuttle/internal/global" +) + +// ExtensionsManager is the entry into installing, updating and using extensions. It is the orchestrator of all the parts that consist of extensions type ExtensionsManager struct { - registry string + globalStore *global.GlobalStore } -func NewExtensionsManager(registry string) *ExtensionsManager { +func NewExtensionsManager(globalStore *global.GlobalStore) *ExtensionsManager { return &ExtensionsManager{ - registry: registry, + globalStore: globalStore, } } +// Init will initialize a repository with a sample extension package func (e *ExtensionsManager) Init(ctx context.Context) error { return nil } +// GetAll will return all known and installed extensions func (e *ExtensionsManager) GetAll(ctx context.Context) ([]Extension, error) { return nil, nil } +// Install will ensure that all known extensions are installed and ready for use func (e *ExtensionsManager) Install(ctx context.Context) error { return nil } -func (e *ExtensionsManager) Update(ctx context.Context) error { +// Update will fetch the latest extensions from a registry and install them afterwards so that they're ready for use +func (e *ExtensionsManager) Update(ctx context.Context, registry string) error { + reg, err := NewRegistry(registry, e.globalStore) + if err != nil { + return fmt.Errorf("failed to update extensions: %w", err) + } + + if err := reg.Update(ctx); err != nil { + return err + } + + // 3. Initiate install + return nil } - -type Extension struct{} diff --git a/internal/extensions/git_registry.go b/internal/extensions/git_registry.go new file mode 100644 index 0000000..ad01098 --- /dev/null +++ b/internal/extensions/git_registry.go @@ -0,0 +1,53 @@ +package extensions + +import ( + "context" + "fmt" + + "github.com/lunarway/shuttle/internal/global" +) + +// gitRegistry represents a type of registry backed by a remote git registry, whether folder or url based. it is denoted by the variable git=github.com/lunarway/shuttle-extensions.git as an example +type gitRegistry struct { + url string + globalStore *global.GlobalStore +} + +func (*gitRegistry) Get(ctx context.Context) error { + panic("unimplemented") +} + +func (g *gitRegistry) Update(ctx context.Context) error { + registry := getRegistryPath(g.globalStore) + + if exists(registry) { + if err := g.fetchGitRepository(ctx); err != nil { + return fmt.Errorf("failed to update registry: %w", err) + } + } else { + if err := ensureExists(registry); err != nil { + return fmt.Errorf("failed to create registry path: %w", err) + } + + if err := g.cloneGitRepository(ctx); err != nil { + return fmt.Errorf("failed to clone registry: %w", err) + } + } + + return nil +} + +func newGitRegistry(url string, globalStore *global.GlobalStore) Registry { + return &gitRegistry{ + url: url, + globalStore: globalStore, + } +} + +func (g *gitRegistry) fetchGitRepository(ctx context.Context) error { + panic("unimplemented") +} + +func (g *gitRegistry) cloneGitRepository(ctx context.Context) error { + panic("unimplemented") +} diff --git a/internal/extensions/global_store_paths.go b/internal/extensions/global_store_paths.go new file mode 100644 index 0000000..b33f781 --- /dev/null +++ b/internal/extensions/global_store_paths.go @@ -0,0 +1,39 @@ +package extensions + +import ( + "errors" + "os" + "path" + + "github.com/lunarway/shuttle/internal/global" +) + +func getRegistryPath(globalStore *global.GlobalStore) string { + return path.Join(globalStore.Root(), "registry") +} + +func getExtensionsPath(globalStore *global.GlobalStore) string { + return path.Join(globalStore.Root(), "extensions") +} + +func getExtensionsCachePath(globalStore *global.GlobalStore) string { + return path.Join(getExtensionsPath(globalStore), "cache") +} + +func ensureExists(dirPath string) error { + return os.MkdirAll(dirPath, 0o666) +} + +func exists(dirPath string) bool { + _, err := os.Stat(dirPath) + + if errors.Is(err, os.ErrNotExist) { + return false + } + + if err != nil { + return false + } + + return true +} diff --git a/internal/extensions/registry.go b/internal/extensions/registry.go new file mode 100644 index 0000000..13fcf21 --- /dev/null +++ b/internal/extensions/registry.go @@ -0,0 +1,30 @@ +package extensions + +import ( + "context" + "fmt" + "strings" + + "github.com/lunarway/shuttle/internal/global" +) + +// Registry represents some kind of upstream registry where extension metadata lives, such as which ones should be downloaded, which versions they're on and how to download them +type Registry interface { + Get(ctx context.Context) error + Update(ctx context.Context) error +} + +// NewRegistry is a shim for concrete implementations of the registries, such as gitRegistry +func NewRegistry(registry string, globalStore *global.GlobalStore) (Registry, error) { + registryType, registryUrl, ok := strings.Cut(registry, "=") + if !ok { + return nil, fmt.Errorf("registry was not a valid url: %s", registry) + } + + switch registryType { + case "git": + return newGitRegistry(registryUrl, globalStore), nil + default: + return nil, fmt.Errorf("registry type was not valid: %s", registryType) + } +} diff --git a/internal/global/global.go b/internal/global/global.go new file mode 100644 index 0000000..eae8ace --- /dev/null +++ b/internal/global/global.go @@ -0,0 +1,41 @@ +package global + +import "os" + +// GlobalStore represents the ~/.shuttle folder it acts as an abstraction for said folder and ensures operations against it are consistent and controlled +type GlobalStore struct { + options *GlobalStoreOptions +} + +type GlobalStoreOption func(options *GlobalStoreOptions) + +func WithShuttleConfig(shuttleConfig string) GlobalStoreOption { + return func(options *GlobalStoreOptions) { + options.ShuttleConfig = shuttleConfig + } +} + +type GlobalStoreOptions struct { + ShuttleConfig string +} + +func newDefaultGlobalStoreOptions() *GlobalStoreOptions { + return &GlobalStoreOptions{ + ShuttleConfig: "$HOME/.shuttle", + } +} + +func NewGlobalStore(options ...GlobalStoreOption) *GlobalStore { + defaultOptions := newDefaultGlobalStoreOptions() + for _, opt := range options { + opt(defaultOptions) + } + + return &GlobalStore{ + options: defaultOptions, + } +} + +func (gs *GlobalStore) Root() string { + return os.ExpandEnv(gs.options.ShuttleConfig) +}