Skip to content

Commit 5a675cd

Browse files
authored
Tab completions (#54)
1 parent dd0d775 commit 5a675cd

5 files changed

Lines changed: 192 additions & 23 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ Major is a platform that lets you deploy and manage access to applications you b
88

99
## Installation
1010

11-
### Homebrew
11+
### Direct Install
1212
```bash
13-
brew tap major-technology/tap
14-
brew install major-technology/tap/major
13+
curl -fsSL https://install.major.build | bash
1514
```
1615

17-
### Direct Install
16+
### Homebrew
1817
```bash
19-
curl -fsSL https://install.major.build | bash
18+
brew tap major-technology/tap
19+
brew install major-technology/tap/major
2020
```
2121

2222
### Updating

cmd/completion.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
var completionCmd = &cobra.Command{
10+
Use: "completion [bash|zsh|fish|powershell]",
11+
Short: "Generate completion script",
12+
Long: `To load completions:
13+
14+
Bash:
15+
16+
$ source <(major completion bash)
17+
18+
# To load completions for each session, execute once:
19+
# Linux:
20+
$ major completion bash > /etc/bash_completion.d/major
21+
# macOS:
22+
$ major completion bash > /usr/local/etc/bash_completion.d/major
23+
24+
Zsh:
25+
26+
# If shell completion is not already enabled in your environment,
27+
# you will need to enable it. You can execute the following once:
28+
29+
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
30+
31+
# To load completions for each session, execute once:
32+
$ major completion zsh > "${fpath[1]}/_major"
33+
34+
# You will need to start a new shell for this setup to take effect.
35+
36+
Fish:
37+
38+
$ major completion fish | source
39+
40+
# To load completions for each session, execute once:
41+
$ major completion fish > ~/.config/fish/completions/major.fish
42+
`,
43+
DisableFlagsInUseLine: true,
44+
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
45+
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
46+
Hidden: true,
47+
Run: func(cmd *cobra.Command, args []string) {
48+
switch args[0] {
49+
case "bash":
50+
cmd.Root().GenBashCompletion(os.Stdout)
51+
case "zsh":
52+
cmd.Root().GenZshCompletion(os.Stdout)
53+
case "fish":
54+
cmd.Root().GenFishCompletion(os.Stdout, true)
55+
case "powershell":
56+
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
57+
}
58+
},
59+
}
60+
61+
func init() {
62+
rootCmd.AddCommand(completionCmd)
63+
}

cmd/install.go

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ func runInstall(cmd *cobra.Command) error {
3030
Bold(true).
3131
Foreground(lipgloss.Color("#00FF00"))
3232

33+
stepStyle := lipgloss.NewStyle().
34+
Foreground(lipgloss.Color("#87D7FF"))
35+
3336
// Get the path to the current executable
3437
exe, err := os.Executable()
3538
if err != nil {
@@ -55,22 +58,19 @@ func runInstall(cmd *cobra.Command) error {
5558
}
5659
}
5760

58-
// If we are in a temp directory or not in the expected location,
59-
// we might want to copy ourselves?
60-
// The script does the downloading, so we assume we are already in ~/.major/bin/major
61-
// We just need to ensure ~/.major/bin is in PATH
62-
6361
home, err := os.UserHomeDir()
6462
if err != nil {
6563
return fmt.Errorf("failed to get user home dir: %w", err)
6664
}
6765

6866
shell := os.Getenv("SHELL")
6967
var configFile string
68+
var shellType string
7069

7170
switch {
7271
case strings.Contains(shell, "zsh"):
7372
configFile = filepath.Join(home, ".zshrc")
73+
shellType = "zsh"
7474
case strings.Contains(shell, "bash"):
7575
configFile = filepath.Join(home, ".bashrc")
7676
// Check for .bash_profile on macOS
@@ -79,18 +79,77 @@ func runInstall(cmd *cobra.Command) error {
7979
configFile = filepath.Join(home, ".bash_profile")
8080
}
8181
}
82+
shellType = "bash"
8283
default:
8384
// Fallback or skip
8485
cmd.Println("Could not detect compatible shell (zsh/bash). Please add the following to your path manually:")
8586
cmd.Printf(" export PATH=\"%s:$PATH\"\n", binDir)
8687
return nil
8788
}
8889

89-
// Check if already in config
90+
// Create completions directory
91+
completionsDir := filepath.Join(home, ".major", "completions")
92+
if err := os.MkdirAll(completionsDir, 0755); err != nil {
93+
return fmt.Errorf("failed to create completions directory: %w", err)
94+
}
95+
96+
// Generate completion script
97+
cmd.Println(stepStyle.Render("▸ Generating shell completions..."))
98+
99+
var completionEntry string
100+
switch shellType {
101+
case "zsh":
102+
// For Zsh, we generate _major file and add directory to fpath
103+
completionFile := filepath.Join(completionsDir, "_major")
104+
f, err := os.Create(completionFile)
105+
if err != nil {
106+
return fmt.Errorf("failed to create zsh completion file: %w", err)
107+
}
108+
defer f.Close()
109+
110+
if err := cmd.Root().GenZshCompletion(f); err != nil {
111+
return fmt.Errorf("failed to generate zsh completion: %w", err)
112+
}
113+
114+
// We need to add fpath before compinit
115+
// But often users already have compinit in their .zshrc
116+
// The safest robust way is to append to fpath and ensure compinit is called
117+
completionEntry = fmt.Sprintf(`
118+
# Major CLI
119+
export PATH="%s:$PATH"
120+
export FPATH="%s:$FPATH"
121+
# Ensure compinit is loaded (if not already)
122+
autoload -U compinit && compinit
123+
`, binDir, completionsDir)
124+
125+
case "bash":
126+
completionFile := filepath.Join(completionsDir, "major.bash")
127+
f, err := os.Create(completionFile)
128+
if err != nil {
129+
return fmt.Errorf("failed to create bash completion file: %w", err)
130+
}
131+
defer f.Close()
132+
133+
if err := cmd.Root().GenBashCompletion(f); err != nil {
134+
return fmt.Errorf("failed to generate bash completion: %w", err)
135+
}
136+
137+
completionEntry = fmt.Sprintf(`
138+
# Major CLI
139+
export PATH="%s:$PATH"
140+
source "%s"
141+
`, binDir, completionFile)
142+
}
143+
144+
// Check if already configured
90145
content, err := os.ReadFile(configFile)
91146
if err == nil {
92-
if strings.Contains(string(content), binDir) {
93-
cmd.Println(successStyle.Render("Major CLI is already in your PATH!"))
147+
// If we already see our marker or the bin path, we might want to update it or skip
148+
// But the user might have moved the directory.
149+
// Let's check for our specific comment
150+
if strings.Contains(string(content), "# Major CLI") {
151+
cmd.Println(successStyle.Render("Major CLI is already configured in your shell!"))
152+
// We still re-generated the completion file above, which is good for updates.
94153
return nil
95154
}
96155
}
@@ -102,13 +161,12 @@ func runInstall(cmd *cobra.Command) error {
102161
}
103162
defer f.Close()
104163

105-
pathEntry := fmt.Sprintf("\n# Major CLI\nexport PATH=\"%s:$PATH\"\n", binDir)
106-
if _, err := f.WriteString(pathEntry); err != nil {
164+
if _, err := f.WriteString(completionEntry); err != nil {
107165
return fmt.Errorf("failed to write to shell config file: %w", err)
108166
}
109167

110-
cmd.Println(successStyle.Render(fmt.Sprintf("Added Major CLI to %s", configFile)))
111-
cmd.Println("Please restart your shell or source your config file to start using 'major'")
168+
cmd.Println(successStyle.Render(fmt.Sprintf("Added Major CLI configuration to %s", configFile)))
169+
cmd.Println("Please restart your shell or run 'source " + configFile + "' to start using 'major'")
112170

113171
return nil
114172
}

cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func Execute() {
7474
func init() {
7575
cobra.OnInitialize(initConfig)
7676

77-
// Disable the completion command
77+
// Disable the default completion command (we use our own)
7878
rootCmd.CompletionOptions.DisableDefaultCmd = true
7979

8080
// Disable the help command (use -h flag instead)

install.sh

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,34 +71,82 @@ if [ -z "$LATEST_TAG" ]; then
7171
exit 1
7272
fi
7373

74-
# Remove 'v' prefix for version number if your assets use strict numbering (major_1.0.0 vs major_v1.0.0)
75-
# GoReleaser usually strips the 'v' in the version template variable {{ .Version }}
74+
# Remove 'v' prefix for version number if your assets use strict numbering
7675
VERSION=${LATEST_TAG#v}
7776

7877
# Construct the asset name
7978
ASSET_NAME="${BINARY}_${VERSION}_${OS}_${ARCH}.tar.gz"
8079
DOWNLOAD_URL="https://github.com/$OWNER/$REPO/releases/download/$LATEST_TAG/$ASSET_NAME"
80+
CHECKSUMS_URL="https://github.com/$OWNER/$REPO/releases/download/$LATEST_TAG/checksums.txt"
8181

8282
print_step "Downloading ${BINARY} ${LATEST_TAG}..."
8383

8484
# Create a temporary directory
8585
TMP_DIR=$(mktemp -d)
86-
curl -fsSL "$DOWNLOAD_URL" -o "$TMP_DIR/$ASSET_NAME" || { print_error "Failed to download from $DOWNLOAD_URL"; exit 1; }
86+
87+
# Download Asset
88+
if ! curl -fsSL "$DOWNLOAD_URL" -o "$TMP_DIR/$ASSET_NAME"; then
89+
print_error "Failed to download binary from $DOWNLOAD_URL"
90+
rm -rf "$TMP_DIR"
91+
exit 1
92+
fi
93+
94+
# Download Checksums
95+
if ! curl -fsSL "$CHECKSUMS_URL" -o "$TMP_DIR/checksums.txt"; then
96+
print_error "Failed to download checksums from $CHECKSUMS_URL"
97+
rm -rf "$TMP_DIR"
98+
exit 1
99+
fi
100+
101+
# Verify Checksum
102+
print_step "Verifying checksum..."
103+
cd "$TMP_DIR"
104+
105+
# Extract the checksum for our specific asset
106+
EXPECTED_CHECKSUM=$(grep "$ASSET_NAME" checksums.txt | awk '{print $1}')
107+
108+
if [ -z "$EXPECTED_CHECKSUM" ]; then
109+
print_error "Could not find checksum for $ASSET_NAME in checksums.txt"
110+
rm -rf "$TMP_DIR"
111+
exit 1
112+
fi
113+
114+
# Calculate actual checksum
115+
if command -v sha256sum >/dev/null 2>&1; then
116+
ACTUAL_CHECKSUM=$(sha256sum "$ASSET_NAME" | awk '{print $1}')
117+
elif command -v shasum >/dev/null 2>&1; then
118+
ACTUAL_CHECKSUM=$(shasum -a 256 "$ASSET_NAME" | awk '{print $1}')
119+
else
120+
print_error "Neither sha256sum nor shasum found to verify checksum"
121+
rm -rf "$TMP_DIR"
122+
exit 1
123+
fi
124+
125+
if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
126+
print_error "Checksum verification failed!"
127+
printf " Expected: %s\n" "$EXPECTED_CHECKSUM"
128+
printf " Actual: %s\n" "$ACTUAL_CHECKSUM"
129+
rm -rf "$TMP_DIR"
130+
exit 1
131+
fi
132+
133+
print_success "Checksum verified"
87134

88135
# Extract and Install
89136
print_step "Installing to $INSTALL_DIR..."
90-
tar -xzf "$TMP_DIR/$ASSET_NAME" -C "$TMP_DIR"
137+
tar -xzf "$ASSET_NAME"
91138

92139
# Create install directory
93140
mkdir -p "$INSTALL_DIR"
94141

95142
# Move binary to install directory
96-
mv "$TMP_DIR/$BINARY" "$INSTALL_DIR/$BINARY"
143+
mv "$BINARY" "$INSTALL_DIR/$BINARY"
97144

98145
# Make sure it's executable
99146
chmod +x "$INSTALL_DIR/$BINARY"
100147

101148
# Cleanup
149+
cd - >/dev/null
102150
rm -rf "$TMP_DIR"
103151

104152
# Run the internal install command to setup shell integration

0 commit comments

Comments
 (0)