Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ Major is a platform that lets you deploy and manage access to applications you b

## Installation

### Homebrew
### Direct Install
```bash
brew tap major-technology/tap
brew install major-technology/tap/major
curl -fsSL https://install.major.build | bash
```

### Direct Install
### Homebrew
```bash
curl -fsSL https://install.major.build | bash
brew tap major-technology/tap
brew install major-technology/tap/major
```

### Updating
Expand Down
63 changes: 63 additions & 0 deletions cmd/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package cmd

import (
"os"

"github.com/spf13/cobra"
)

var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: `To load completions:

Bash:

$ source <(major completion bash)

# To load completions for each session, execute once:
# Linux:
$ major completion bash > /etc/bash_completion.d/major
# macOS:
$ major completion bash > /usr/local/etc/bash_completion.d/major

Zsh:

# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:

$ echo "autoload -U compinit; compinit" >> ~/.zshrc

# To load completions for each session, execute once:
$ major completion zsh > "${fpath[1]}/_major"

# You will need to start a new shell for this setup to take effect.

Fish:

$ major completion fish | source

# To load completions for each session, execute once:
$ major completion fish > ~/.config/fish/completions/major.fish
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}

func init() {
rootCmd.AddCommand(completionCmd)
}
82 changes: 70 additions & 12 deletions cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ func runInstall(cmd *cobra.Command) error {
Bold(true).
Foreground(lipgloss.Color("#00FF00"))

stepStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#87D7FF"))

// Get the path to the current executable
exe, err := os.Executable()
if err != nil {
Expand All @@ -55,22 +58,19 @@ func runInstall(cmd *cobra.Command) error {
}
}

// If we are in a temp directory or not in the expected location,
// we might want to copy ourselves?
// The script does the downloading, so we assume we are already in ~/.major/bin/major
// We just need to ensure ~/.major/bin is in PATH

home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home dir: %w", err)
}

shell := os.Getenv("SHELL")
var configFile string
var shellType string

switch {
case strings.Contains(shell, "zsh"):
configFile = filepath.Join(home, ".zshrc")
shellType = "zsh"
case strings.Contains(shell, "bash"):
configFile = filepath.Join(home, ".bashrc")
// Check for .bash_profile on macOS
Expand All @@ -79,18 +79,77 @@ func runInstall(cmd *cobra.Command) error {
configFile = filepath.Join(home, ".bash_profile")
}
}
shellType = "bash"
default:
// Fallback or skip
cmd.Println("Could not detect compatible shell (zsh/bash). Please add the following to your path manually:")
cmd.Printf(" export PATH=\"%s:$PATH\"\n", binDir)
return nil
}

// Check if already in config
// Create completions directory
completionsDir := filepath.Join(home, ".major", "completions")
if err := os.MkdirAll(completionsDir, 0755); err != nil {
return fmt.Errorf("failed to create completions directory: %w", err)
}

// Generate completion script
cmd.Println(stepStyle.Render("▸ Generating shell completions..."))

var completionEntry string
switch shellType {
case "zsh":
// For Zsh, we generate _major file and add directory to fpath
completionFile := filepath.Join(completionsDir, "_major")
f, err := os.Create(completionFile)
if err != nil {
return fmt.Errorf("failed to create zsh completion file: %w", err)
}
defer f.Close()

if err := cmd.Root().GenZshCompletion(f); err != nil {
return fmt.Errorf("failed to generate zsh completion: %w", err)
}

// We need to add fpath before compinit
// But often users already have compinit in their .zshrc
// The safest robust way is to append to fpath and ensure compinit is called
completionEntry = fmt.Sprintf(`
# Major CLI
export PATH="%s:$PATH"
export FPATH="%s:$FPATH"
# Ensure compinit is loaded (if not already)
autoload -U compinit && compinit
`, binDir, completionsDir)

case "bash":
completionFile := filepath.Join(completionsDir, "major.bash")
f, err := os.Create(completionFile)
if err != nil {
return fmt.Errorf("failed to create bash completion file: %w", err)
}
defer f.Close()

if err := cmd.Root().GenBashCompletion(f); err != nil {
return fmt.Errorf("failed to generate bash completion: %w", err)
}

completionEntry = fmt.Sprintf(`
# Major CLI
export PATH="%s:$PATH"
source "%s"
`, binDir, completionFile)
}

// Check if already configured
content, err := os.ReadFile(configFile)
if err == nil {
if strings.Contains(string(content), binDir) {
cmd.Println(successStyle.Render("Major CLI is already in your PATH!"))
// If we already see our marker or the bin path, we might want to update it or skip
// But the user might have moved the directory.
// Let's check for our specific comment
if strings.Contains(string(content), "# Major CLI") {
cmd.Println(successStyle.Render("Major CLI is already configured in your shell!"))
// We still re-generated the completion file above, which is good for updates.
return nil
}
}
Expand All @@ -102,13 +161,12 @@ func runInstall(cmd *cobra.Command) error {
}
defer f.Close()

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

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

return nil
}
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func Execute() {
func init() {
cobra.OnInitialize(initConfig)

// Disable the completion command
// Disable the default completion command (we use our own)
rootCmd.CompletionOptions.DisableDefaultCmd = true

// Disable the help command (use -h flag instead)
Expand Down
58 changes: 53 additions & 5 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,34 +71,82 @@ if [ -z "$LATEST_TAG" ]; then
exit 1
fi

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

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

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

# Create a temporary directory
TMP_DIR=$(mktemp -d)
curl -fsSL "$DOWNLOAD_URL" -o "$TMP_DIR/$ASSET_NAME" || { print_error "Failed to download from $DOWNLOAD_URL"; exit 1; }

# Download Asset
if ! curl -fsSL "$DOWNLOAD_URL" -o "$TMP_DIR/$ASSET_NAME"; then
print_error "Failed to download binary from $DOWNLOAD_URL"
rm -rf "$TMP_DIR"
exit 1
fi

# Download Checksums
if ! curl -fsSL "$CHECKSUMS_URL" -o "$TMP_DIR/checksums.txt"; then
print_error "Failed to download checksums from $CHECKSUMS_URL"
rm -rf "$TMP_DIR"
exit 1
fi

# Verify Checksum
print_step "Verifying checksum..."
cd "$TMP_DIR"

# Extract the checksum for our specific asset
EXPECTED_CHECKSUM=$(grep "$ASSET_NAME" checksums.txt | awk '{print $1}')

if [ -z "$EXPECTED_CHECKSUM" ]; then
print_error "Could not find checksum for $ASSET_NAME in checksums.txt"
rm -rf "$TMP_DIR"
exit 1
fi

# Calculate actual checksum
if command -v sha256sum >/dev/null 2>&1; then
ACTUAL_CHECKSUM=$(sha256sum "$ASSET_NAME" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
ACTUAL_CHECKSUM=$(shasum -a 256 "$ASSET_NAME" | awk '{print $1}')
else
print_error "Neither sha256sum nor shasum found to verify checksum"
rm -rf "$TMP_DIR"
exit 1
fi

if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
print_error "Checksum verification failed!"
printf " Expected: %s\n" "$EXPECTED_CHECKSUM"
printf " Actual: %s\n" "$ACTUAL_CHECKSUM"
rm -rf "$TMP_DIR"
exit 1
fi

print_success "Checksum verified"

# Extract and Install
print_step "Installing to $INSTALL_DIR..."
tar -xzf "$TMP_DIR/$ASSET_NAME" -C "$TMP_DIR"
tar -xzf "$ASSET_NAME"

# Create install directory
mkdir -p "$INSTALL_DIR"

# Move binary to install directory
mv "$TMP_DIR/$BINARY" "$INSTALL_DIR/$BINARY"
mv "$BINARY" "$INSTALL_DIR/$BINARY"

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

# Cleanup
cd - >/dev/null
rm -rf "$TMP_DIR"

# Run the internal install command to setup shell integration
Expand Down