-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
4 changed files
with
408 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
package extensions | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/XiaoConstantine/mycli/pkg/iostreams" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
const ExtensionPrefix = "mycli-" | ||
|
||
type Extension struct { | ||
Name string | ||
Path string | ||
} | ||
|
||
func isExecutable(path string) bool { | ||
info, err := os.Stat(path) | ||
if err != nil { | ||
return false | ||
} | ||
return info.Mode().IsRegular() && (info.Mode().Perm()&0111 != 0) | ||
} | ||
|
||
func IsExtension(path string) bool { | ||
base := filepath.Base(path) | ||
return strings.HasPrefix(base, ExtensionPrefix) && isExecutable(path) | ||
} | ||
|
||
func GetExtensionsDir() string { | ||
home, _ := os.UserHomeDir() | ||
return filepath.Join(home, ".mycli", "extensions") | ||
} | ||
|
||
func (e *Extension) Execute(args []string) error { | ||
cmd := exec.Command(e.Path, args...) | ||
cmd.Stdin = os.Stdin | ||
cmd.Stdout = os.Stdout | ||
cmd.Stderr = os.Stderr | ||
return cmd.Run() | ||
} | ||
|
||
func NewCmdExtension(iostream *iostreams.IOStreams) *cobra.Command { | ||
cmd := &cobra.Command{ | ||
Use: "extension", | ||
Short: "Manage mycli extensions", | ||
} | ||
|
||
cmd.AddCommand(newExtensionInstallCmd(iostream)) | ||
cmd.AddCommand(newExtensionListCmd(iostream)) | ||
cmd.AddCommand(newExtensionRemoveCmd(iostream)) | ||
cmd.AddCommand(newExtensionUpdateCmd(iostream)) | ||
|
||
return cmd | ||
} | ||
|
||
func newExtensionInstallCmd(iostream *iostreams.IOStreams) *cobra.Command { | ||
return &cobra.Command{ | ||
Use: "install <repository>", | ||
Short: "Install a mycli extension", | ||
Args: cobra.ExactArgs(1), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
repo := args[0] | ||
extDir := GetExtensionsDir() | ||
extName := filepath.Base(repo) | ||
extPath := filepath.Join(extDir, ExtensionPrefix+extName) | ||
|
||
if err := os.MkdirAll(extDir, 0755); err != nil { | ||
return fmt.Errorf("failed to create extensions directory: %w", err) | ||
} | ||
|
||
gitCmd := exec.Command("git", "clone", repo, extPath) | ||
gitCmd.Stdout = iostream.Out | ||
gitCmd.Stderr = iostream.ErrOut | ||
|
||
if err := gitCmd.Run(); err != nil { | ||
return fmt.Errorf("failed to clone extension repository: %w", err) | ||
} | ||
|
||
fmt.Fprintf(iostream.Out, "Successfully installed extension '%s'\n", extName) | ||
return nil | ||
}, | ||
} | ||
} | ||
|
||
func newExtensionListCmd(iostream *iostreams.IOStreams) *cobra.Command { | ||
return &cobra.Command{ | ||
Use: "list", | ||
Short: "List installed mycli extensions", | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
extDir := GetExtensionsDir() | ||
entries, err := os.ReadDir(extDir) | ||
if err != nil { | ||
if os.IsNotExist(err) { | ||
fmt.Fprintln(iostream.Out, "No extensions installed") | ||
return nil | ||
} | ||
return fmt.Errorf("failed to read extensions directory: %w", err) | ||
} | ||
|
||
for _, entry := range entries { | ||
if entry.IsDir() && strings.HasPrefix(entry.Name(), ExtensionPrefix) { | ||
fmt.Fprintln(iostream.Out, entry.Name()[len(ExtensionPrefix):]) | ||
} | ||
} | ||
return nil | ||
}, | ||
} | ||
} | ||
|
||
func newExtensionRemoveCmd(iostream *iostreams.IOStreams) *cobra.Command { | ||
return &cobra.Command{ | ||
Use: "remove <extension-name>", | ||
Short: "Remove a mycli extension", | ||
Args: cobra.ExactArgs(1), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
extName := args[0] | ||
extDir := GetExtensionsDir() | ||
extPath := filepath.Join(extDir, ExtensionPrefix+extName) | ||
|
||
if err := os.RemoveAll(extPath); err != nil { | ||
return fmt.Errorf("failed to remove extension: %w", err) | ||
} | ||
|
||
fmt.Fprintf(iostream.Out, "Successfully removed extension '%s'\n", extName) | ||
return nil | ||
}, | ||
} | ||
} | ||
|
||
func newExtensionUpdateCmd(iostream *iostreams.IOStreams) *cobra.Command { | ||
return &cobra.Command{ | ||
Use: "update <extension-name>", | ||
Short: "Update a mycli extension", | ||
Args: cobra.ExactArgs(1), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
extName := args[0] | ||
extDir := GetExtensionsDir() | ||
extPath := filepath.Join(extDir, ExtensionPrefix+extName) | ||
|
||
gitCmd := exec.Command("git", "-C", extPath, "pull") | ||
gitCmd.Stdout = iostream.Out | ||
gitCmd.Stderr = iostream.ErrOut | ||
|
||
if err := gitCmd.Run(); err != nil { | ||
return fmt.Errorf("failed to update extension: %w", err) | ||
} | ||
|
||
fmt.Fprintf(iostream.Out, "Successfully updated extension '%s'\n", extName) | ||
return nil | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package extensions | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestGetExtensionsDir(t *testing.T) { | ||
home, err := os.UserHomeDir() | ||
assert.NoError(t, err) | ||
|
||
expected := filepath.Join(home, ".mycli", "extensions") | ||
result := GetExtensionsDir() | ||
|
||
assert.Equal(t, expected, result) | ||
} | ||
|
||
func TestExtensionExecute(t *testing.T) { | ||
// Create a temporary directory for testing | ||
tempDir, err := os.MkdirTemp("", "mycli-test") | ||
assert.NoError(t, err) | ||
defer os.RemoveAll(tempDir) | ||
|
||
// Create a mock executable file | ||
mockExecutable := filepath.Join(tempDir, ExtensionPrefix+"mock") | ||
mockContent := []byte("#!/bin/sh\necho 'Mock executed'") | ||
err = os.WriteFile(mockExecutable, mockContent, 0755) | ||
assert.NoError(t, err) | ||
|
||
ext := &Extension{ | ||
Name: "mock", | ||
Path: mockExecutable, | ||
} | ||
|
||
// Test execution | ||
err = ext.Execute([]string{"arg1", "arg2"}) | ||
assert.NoError(t, err) | ||
|
||
// Test execution with non-existent file | ||
ext.Path = filepath.Join(tempDir, "non-existent") | ||
err = ext.Execute([]string{}) | ||
assert.Error(t, err) | ||
} | ||
|
||
func TestIsExecutable(t *testing.T) { | ||
// Create a temporary directory for testing | ||
tempDir, err := os.MkdirTemp("", "mycli-test") | ||
assert.NoError(t, err) | ||
defer os.RemoveAll(tempDir) | ||
|
||
// Test cases | ||
testCases := []struct { | ||
name string | ||
fileName string | ||
perms os.FileMode | ||
expected bool | ||
}{ | ||
{"Executable file", "exec", 0755, true}, | ||
{"Non-executable file", "non-exec", 0644, false}, | ||
{"Directory", "dir", 0755, false}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
path := filepath.Join(tempDir, tc.fileName) | ||
if tc.name == "Directory" { | ||
err = os.Mkdir(path, tc.perms) | ||
} else { | ||
err = os.WriteFile(path, []byte("test content"), tc.perms) | ||
} | ||
assert.NoError(t, err) | ||
|
||
result := isExecutable(path) | ||
assert.Equal(t, tc.expected, result) | ||
}) | ||
} | ||
|
||
// Test non-existent file | ||
t.Run("Non-existent file", func(t *testing.T) { | ||
result := isExecutable(filepath.Join(tempDir, "non-existent")) | ||
assert.False(t, result) | ||
}) | ||
} | ||
|
||
func TestIsExtension(t *testing.T) { | ||
// Create a temporary directory for testing | ||
tempDir, err := os.MkdirTemp("", "mycli-test") | ||
assert.NoError(t, err) | ||
defer os.RemoveAll(tempDir) | ||
|
||
// Test cases | ||
testCases := []struct { | ||
name string | ||
fileName string | ||
setup func(string) error | ||
expected bool | ||
}{ | ||
{ | ||
name: "Valid extension", | ||
fileName: ExtensionPrefix + "test", | ||
setup: func(path string) error { | ||
return os.WriteFile(path, []byte("test content"), 0755) | ||
}, | ||
expected: true, | ||
}, | ||
{ | ||
name: "Non-executable extension", | ||
fileName: ExtensionPrefix + "test", | ||
setup: func(path string) error { | ||
if err := os.WriteFile(path, []byte("test content"), 0644); err != nil { | ||
return err | ||
} | ||
// Explicitly remove execute permissions | ||
return os.Chmod(path, 0644) | ||
}, | ||
expected: false, | ||
}, | ||
{ | ||
name: "Non-prefix file", | ||
fileName: "test", | ||
setup: func(path string) error { | ||
return os.WriteFile(path, []byte("test content"), 0755) | ||
}, | ||
expected: false, | ||
}, | ||
{ | ||
name: "Directory with prefix", | ||
fileName: ExtensionPrefix + "dir", | ||
setup: func(path string) error { | ||
return os.Mkdir(path, 0755) | ||
}, | ||
expected: false, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
path := filepath.Join(tempDir, tc.fileName) | ||
err := tc.setup(path) | ||
assert.NoError(t, err) | ||
|
||
result := IsExtension(path) | ||
assert.Equal(t, tc.expected, result) | ||
}) | ||
} | ||
|
||
// Test non-existent file | ||
t.Run("Non-existent file", func(t *testing.T) { | ||
result := IsExtension(filepath.Join(tempDir, "non-existent")) | ||
assert.False(t, result) | ||
}) | ||
} |
Oops, something went wrong.