|
|
|
|
|
|
|
|
package kiro |
|
|
|
|
|
import ( |
|
|
"context" |
|
|
"fmt" |
|
|
"html" |
|
|
"net" |
|
|
"net/http" |
|
|
"net/url" |
|
|
"os" |
|
|
"os/exec" |
|
|
"path/filepath" |
|
|
"runtime" |
|
|
"strings" |
|
|
"sync" |
|
|
"time" |
|
|
|
|
|
log "github.com/sirupsen/logrus" |
|
|
) |
|
|
|
|
|
const ( |
|
|
|
|
|
KiroProtocol = "kiro" |
|
|
|
|
|
|
|
|
KiroAuthority = "kiro.kiroAgent" |
|
|
|
|
|
|
|
|
KiroAuthPath = "/authenticate-success" |
|
|
|
|
|
|
|
|
KiroRedirectURI = "kiro://kiro.kiroAgent/authenticate-success" |
|
|
|
|
|
|
|
|
DefaultHandlerPort = 19876 |
|
|
|
|
|
|
|
|
HandlerTimeout = 10 * time.Minute |
|
|
) |
|
|
|
|
|
|
|
|
type ProtocolHandler struct { |
|
|
port int |
|
|
server *http.Server |
|
|
listener net.Listener |
|
|
resultChan chan *AuthCallback |
|
|
stopChan chan struct{} |
|
|
mu sync.Mutex |
|
|
running bool |
|
|
} |
|
|
|
|
|
|
|
|
type AuthCallback struct { |
|
|
Code string |
|
|
State string |
|
|
Error string |
|
|
} |
|
|
|
|
|
|
|
|
func NewProtocolHandler() *ProtocolHandler { |
|
|
return &ProtocolHandler{ |
|
|
port: DefaultHandlerPort, |
|
|
resultChan: make(chan *AuthCallback, 1), |
|
|
stopChan: make(chan struct{}), |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func (h *ProtocolHandler) Start(ctx context.Context) (int, error) { |
|
|
h.mu.Lock() |
|
|
defer h.mu.Unlock() |
|
|
|
|
|
if h.running { |
|
|
return h.port, nil |
|
|
} |
|
|
|
|
|
|
|
|
select { |
|
|
case <-h.resultChan: |
|
|
default: |
|
|
} |
|
|
|
|
|
|
|
|
if h.stopChan != nil { |
|
|
select { |
|
|
case <-h.stopChan: |
|
|
|
|
|
default: |
|
|
close(h.stopChan) |
|
|
} |
|
|
} |
|
|
h.stopChan = make(chan struct{}) |
|
|
|
|
|
|
|
|
var listener net.Listener |
|
|
var err error |
|
|
portRange := []int{DefaultHandlerPort, DefaultHandlerPort + 1, DefaultHandlerPort + 2, DefaultHandlerPort + 3, DefaultHandlerPort + 4} |
|
|
|
|
|
for _, port := range portRange { |
|
|
listener, err = net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) |
|
|
if err == nil { |
|
|
break |
|
|
} |
|
|
log.Debugf("kiro protocol handler: port %d busy, trying next", port) |
|
|
} |
|
|
|
|
|
if listener == nil { |
|
|
return 0, fmt.Errorf("failed to start callback server: all ports %d-%d are busy", DefaultHandlerPort, DefaultHandlerPort+4) |
|
|
} |
|
|
|
|
|
h.listener = listener |
|
|
h.port = listener.Addr().(*net.TCPAddr).Port |
|
|
|
|
|
mux := http.NewServeMux() |
|
|
mux.HandleFunc("/oauth/callback", h.handleCallback) |
|
|
|
|
|
h.server = &http.Server{ |
|
|
Handler: mux, |
|
|
ReadHeaderTimeout: 10 * time.Second, |
|
|
} |
|
|
|
|
|
go func() { |
|
|
if err := h.server.Serve(listener); err != nil && err != http.ErrServerClosed { |
|
|
log.Debugf("kiro protocol handler server error: %v", err) |
|
|
} |
|
|
}() |
|
|
|
|
|
h.running = true |
|
|
log.Debugf("kiro protocol handler started on port %d", h.port) |
|
|
|
|
|
|
|
|
|
|
|
currentStopChan := h.stopChan |
|
|
currentServer := h.server |
|
|
currentListener := h.listener |
|
|
go func() { |
|
|
select { |
|
|
case <-ctx.Done(): |
|
|
case <-time.After(HandlerTimeout): |
|
|
case <-currentStopChan: |
|
|
return |
|
|
} |
|
|
|
|
|
h.mu.Lock() |
|
|
if h.server == currentServer && h.listener == currentListener { |
|
|
h.mu.Unlock() |
|
|
h.Stop() |
|
|
} else { |
|
|
h.mu.Unlock() |
|
|
} |
|
|
}() |
|
|
|
|
|
return h.port, nil |
|
|
} |
|
|
|
|
|
|
|
|
func (h *ProtocolHandler) Stop() { |
|
|
h.mu.Lock() |
|
|
defer h.mu.Unlock() |
|
|
|
|
|
if !h.running { |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
select { |
|
|
case <-h.stopChan: |
|
|
|
|
|
default: |
|
|
close(h.stopChan) |
|
|
} |
|
|
|
|
|
if h.server != nil { |
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
|
|
defer cancel() |
|
|
_ = h.server.Shutdown(ctx) |
|
|
} |
|
|
|
|
|
h.running = false |
|
|
log.Debug("kiro protocol handler stopped") |
|
|
} |
|
|
|
|
|
|
|
|
func (h *ProtocolHandler) WaitForCallback(ctx context.Context) (*AuthCallback, error) { |
|
|
select { |
|
|
case <-ctx.Done(): |
|
|
return nil, ctx.Err() |
|
|
case <-time.After(HandlerTimeout): |
|
|
return nil, fmt.Errorf("timeout waiting for OAuth callback") |
|
|
case result := <-h.resultChan: |
|
|
return result, nil |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func (h *ProtocolHandler) GetPort() int { |
|
|
return h.port |
|
|
} |
|
|
|
|
|
|
|
|
func (h *ProtocolHandler) handleCallback(w http.ResponseWriter, r *http.Request) { |
|
|
code := r.URL.Query().Get("code") |
|
|
state := r.URL.Query().Get("state") |
|
|
errParam := r.URL.Query().Get("error") |
|
|
|
|
|
result := &AuthCallback{ |
|
|
Code: code, |
|
|
State: state, |
|
|
Error: errParam, |
|
|
} |
|
|
|
|
|
|
|
|
select { |
|
|
case h.resultChan <- result: |
|
|
default: |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8") |
|
|
if errParam != "" { |
|
|
w.WriteHeader(http.StatusBadRequest) |
|
|
fmt.Fprintf(w, `<!DOCTYPE html> |
|
|
<html> |
|
|
<head><title>Login Failed</title></head> |
|
|
<body> |
|
|
<h1>Login Failed</h1> |
|
|
<p>Error: %s</p> |
|
|
<p>You can close this window.</p> |
|
|
</body> |
|
|
</html>`, html.EscapeString(errParam)) |
|
|
} else { |
|
|
fmt.Fprint(w, `<!DOCTYPE html> |
|
|
<html> |
|
|
<head><title>Login Successful</title></head> |
|
|
<body> |
|
|
<h1>Login Successful!</h1> |
|
|
<p>You can close this window and return to the terminal.</p> |
|
|
<script>window.close();</script> |
|
|
</body> |
|
|
</html>`) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func IsProtocolHandlerInstalled() bool { |
|
|
switch runtime.GOOS { |
|
|
case "linux": |
|
|
return isLinuxHandlerInstalled() |
|
|
case "windows": |
|
|
return isWindowsHandlerInstalled() |
|
|
case "darwin": |
|
|
return isDarwinHandlerInstalled() |
|
|
default: |
|
|
return false |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func InstallProtocolHandler(handlerPort int) error { |
|
|
switch runtime.GOOS { |
|
|
case "linux": |
|
|
return installLinuxHandler(handlerPort) |
|
|
case "windows": |
|
|
return installWindowsHandler(handlerPort) |
|
|
case "darwin": |
|
|
return installDarwinHandler(handlerPort) |
|
|
default: |
|
|
return fmt.Errorf("unsupported platform: %s", runtime.GOOS) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func UninstallProtocolHandler() error { |
|
|
switch runtime.GOOS { |
|
|
case "linux": |
|
|
return uninstallLinuxHandler() |
|
|
case "windows": |
|
|
return uninstallWindowsHandler() |
|
|
case "darwin": |
|
|
return uninstallDarwinHandler() |
|
|
default: |
|
|
return fmt.Errorf("unsupported platform: %s", runtime.GOOS) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func getLinuxDesktopPath() string { |
|
|
homeDir, _ := os.UserHomeDir() |
|
|
return filepath.Join(homeDir, ".local", "share", "applications", "kiro-oauth-handler.desktop") |
|
|
} |
|
|
|
|
|
func getLinuxHandlerScriptPath() string { |
|
|
homeDir, _ := os.UserHomeDir() |
|
|
return filepath.Join(homeDir, ".local", "bin", "kiro-oauth-handler") |
|
|
} |
|
|
|
|
|
func isLinuxHandlerInstalled() bool { |
|
|
desktopPath := getLinuxDesktopPath() |
|
|
_, err := os.Stat(desktopPath) |
|
|
return err == nil |
|
|
} |
|
|
|
|
|
func installLinuxHandler(handlerPort int) error { |
|
|
|
|
|
homeDir, err := os.UserHomeDir() |
|
|
if err != nil { |
|
|
return err |
|
|
} |
|
|
|
|
|
binDir := filepath.Join(homeDir, ".local", "bin") |
|
|
appDir := filepath.Join(homeDir, ".local", "share", "applications") |
|
|
|
|
|
if err := os.MkdirAll(binDir, 0755); err != nil { |
|
|
return fmt.Errorf("failed to create bin directory: %w", err) |
|
|
} |
|
|
if err := os.MkdirAll(appDir, 0755); err != nil { |
|
|
return fmt.Errorf("failed to create applications directory: %w", err) |
|
|
} |
|
|
|
|
|
|
|
|
scriptPath := getLinuxHandlerScriptPath() |
|
|
scriptContent := fmt.Sprintf(`#!/bin/bash |
|
|
# Kiro OAuth Protocol Handler |
|
|
# Handles kiro:// URIs - tries CLI first, then forwards to Kiro IDE |
|
|
|
|
|
URL="$1" |
|
|
|
|
|
# Check curl availability |
|
|
if ! command -v curl &> /dev/null; then |
|
|
echo "Error: curl is required for Kiro OAuth handler" >&2 |
|
|
exit 1 |
|
|
fi |
|
|
|
|
|
# Extract code and state from URL |
|
|
[[ "$URL" =~ code=([^&]+) ]] && CODE="${BASH_REMATCH[1]}" |
|
|
[[ "$URL" =~ state=([^&]+) ]] && STATE="${BASH_REMATCH[1]}" |
|
|
[[ "$URL" =~ error=([^&]+) ]] && ERROR="${BASH_REMATCH[1]}" |
|
|
|
|
|
# Try CLI proxy on multiple possible ports (default + dynamic range) |
|
|
CLI_OK=0 |
|
|
for PORT in %d %d %d %d %d; do |
|
|
if [ -n "$ERROR" ]; then |
|
|
curl -sf --connect-timeout 1 "http://127.0.0.1:$PORT/oauth/callback?error=$ERROR" && CLI_OK=1 && break |
|
|
elif [ -n "$CODE" ] && [ -n "$STATE" ]; then |
|
|
curl -sf --connect-timeout 1 "http://127.0.0.1:$PORT/oauth/callback?code=$CODE&state=$STATE" && CLI_OK=1 && break |
|
|
fi |
|
|
done |
|
|
|
|
|
# If CLI not available, forward to Kiro IDE |
|
|
if [ $CLI_OK -eq 0 ] && [ -x "/usr/share/kiro/kiro" ]; then |
|
|
/usr/share/kiro/kiro --open-url "$URL" & |
|
|
fi |
|
|
`, handlerPort, handlerPort+1, handlerPort+2, handlerPort+3, handlerPort+4) |
|
|
|
|
|
if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil { |
|
|
return fmt.Errorf("failed to write handler script: %w", err) |
|
|
} |
|
|
|
|
|
|
|
|
desktopPath := getLinuxDesktopPath() |
|
|
desktopContent := fmt.Sprintf(`[Desktop Entry] |
|
|
Name=Kiro OAuth Handler |
|
|
Comment=Handle kiro:// protocol for CLI Proxy API authentication |
|
|
Exec=%s %%u |
|
|
Type=Application |
|
|
Terminal=false |
|
|
NoDisplay=true |
|
|
MimeType=x-scheme-handler/kiro; |
|
|
Categories=Utility; |
|
|
`, scriptPath) |
|
|
|
|
|
if err := os.WriteFile(desktopPath, []byte(desktopContent), 0644); err != nil { |
|
|
return fmt.Errorf("failed to write desktop file: %w", err) |
|
|
} |
|
|
|
|
|
|
|
|
cmd := exec.Command("xdg-mime", "default", "kiro-oauth-handler.desktop", "x-scheme-handler/kiro") |
|
|
if err := cmd.Run(); err != nil { |
|
|
log.Warnf("xdg-mime registration failed (may need manual setup): %v", err) |
|
|
} |
|
|
|
|
|
|
|
|
cmd = exec.Command("update-desktop-database", appDir) |
|
|
_ = cmd.Run() |
|
|
|
|
|
log.Info("Kiro protocol handler installed for Linux") |
|
|
return nil |
|
|
} |
|
|
|
|
|
func uninstallLinuxHandler() error { |
|
|
desktopPath := getLinuxDesktopPath() |
|
|
scriptPath := getLinuxHandlerScriptPath() |
|
|
|
|
|
if err := os.Remove(desktopPath); err != nil && !os.IsNotExist(err) { |
|
|
return fmt.Errorf("failed to remove desktop file: %w", err) |
|
|
} |
|
|
if err := os.Remove(scriptPath); err != nil && !os.IsNotExist(err) { |
|
|
return fmt.Errorf("failed to remove handler script: %w", err) |
|
|
} |
|
|
|
|
|
log.Info("Kiro protocol handler uninstalled") |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func isWindowsHandlerInstalled() bool { |
|
|
|
|
|
cmd := exec.Command("reg", "query", `HKCU\Software\Classes\kiro`, "/ve") |
|
|
return cmd.Run() == nil |
|
|
} |
|
|
|
|
|
func installWindowsHandler(handlerPort int) error { |
|
|
homeDir, err := os.UserHomeDir() |
|
|
if err != nil { |
|
|
return err |
|
|
} |
|
|
|
|
|
|
|
|
scriptDir := filepath.Join(homeDir, ".cliproxyapi") |
|
|
if err := os.MkdirAll(scriptDir, 0755); err != nil { |
|
|
return fmt.Errorf("failed to create script directory: %w", err) |
|
|
} |
|
|
|
|
|
scriptPath := filepath.Join(scriptDir, "kiro-oauth-handler.ps1") |
|
|
scriptContent := fmt.Sprintf(`# Kiro OAuth Protocol Handler for Windows |
|
|
param([string]$url) |
|
|
|
|
|
# Load required assembly for HttpUtility |
|
|
Add-Type -AssemblyName System.Web |
|
|
|
|
|
# Parse URL parameters |
|
|
$uri = [System.Uri]$url |
|
|
$query = [System.Web.HttpUtility]::ParseQueryString($uri.Query) |
|
|
$code = $query["code"] |
|
|
$state = $query["state"] |
|
|
$errorParam = $query["error"] |
|
|
|
|
|
# Try multiple ports (default + dynamic range) |
|
|
$ports = @(%d, %d, %d, %d, %d) |
|
|
$success = $false |
|
|
|
|
|
foreach ($port in $ports) { |
|
|
if ($success) { break } |
|
|
$callbackUrl = "http://127.0.0.1:$port/oauth/callback" |
|
|
try { |
|
|
if ($errorParam) { |
|
|
$fullUrl = $callbackUrl + "?error=" + $errorParam |
|
|
Invoke-WebRequest -Uri $fullUrl -UseBasicParsing -TimeoutSec 1 -ErrorAction Stop | Out-Null |
|
|
$success = $true |
|
|
} elseif ($code -and $state) { |
|
|
$fullUrl = $callbackUrl + "?code=" + $code + "&state=" + $state |
|
|
Invoke-WebRequest -Uri $fullUrl -UseBasicParsing -TimeoutSec 1 -ErrorAction Stop | Out-Null |
|
|
$success = $true |
|
|
} |
|
|
} catch { |
|
|
# Try next port |
|
|
} |
|
|
} |
|
|
`, handlerPort, handlerPort+1, handlerPort+2, handlerPort+3, handlerPort+4) |
|
|
|
|
|
if err := os.WriteFile(scriptPath, []byte(scriptContent), 0644); err != nil { |
|
|
return fmt.Errorf("failed to write handler script: %w", err) |
|
|
} |
|
|
|
|
|
|
|
|
batchPath := filepath.Join(scriptDir, "kiro-oauth-handler.bat") |
|
|
batchContent := fmt.Sprintf("@echo off\npowershell -ExecutionPolicy Bypass -File \"%s\" %%1\n", scriptPath) |
|
|
|
|
|
if err := os.WriteFile(batchPath, []byte(batchContent), 0644); err != nil { |
|
|
return fmt.Errorf("failed to write batch wrapper: %w", err) |
|
|
} |
|
|
|
|
|
|
|
|
commands := [][]string{ |
|
|
{"reg", "add", `HKCU\Software\Classes\kiro`, "/ve", "/d", "URL:Kiro Protocol", "/f"}, |
|
|
{"reg", "add", `HKCU\Software\Classes\kiro`, "/v", "URL Protocol", "/d", "", "/f"}, |
|
|
{"reg", "add", `HKCU\Software\Classes\kiro\shell`, "/f"}, |
|
|
{"reg", "add", `HKCU\Software\Classes\kiro\shell\open`, "/f"}, |
|
|
{"reg", "add", `HKCU\Software\Classes\kiro\shell\open\command`, "/ve", "/d", fmt.Sprintf("\"%s\" \"%%1\"", batchPath), "/f"}, |
|
|
} |
|
|
|
|
|
for _, args := range commands { |
|
|
cmd := exec.Command(args[0], args[1:]...) |
|
|
if err := cmd.Run(); err != nil { |
|
|
return fmt.Errorf("failed to run registry command: %w", err) |
|
|
} |
|
|
} |
|
|
|
|
|
log.Info("Kiro protocol handler installed for Windows") |
|
|
return nil |
|
|
} |
|
|
|
|
|
func uninstallWindowsHandler() error { |
|
|
|
|
|
cmd := exec.Command("reg", "delete", `HKCU\Software\Classes\kiro`, "/f") |
|
|
if err := cmd.Run(); err != nil { |
|
|
log.Warnf("failed to remove registry key: %v", err) |
|
|
} |
|
|
|
|
|
|
|
|
homeDir, _ := os.UserHomeDir() |
|
|
scriptDir := filepath.Join(homeDir, ".cliproxyapi") |
|
|
_ = os.Remove(filepath.Join(scriptDir, "kiro-oauth-handler.ps1")) |
|
|
_ = os.Remove(filepath.Join(scriptDir, "kiro-oauth-handler.bat")) |
|
|
|
|
|
log.Info("Kiro protocol handler uninstalled") |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func getDarwinAppPath() string { |
|
|
homeDir, _ := os.UserHomeDir() |
|
|
return filepath.Join(homeDir, "Applications", "KiroOAuthHandler.app") |
|
|
} |
|
|
|
|
|
func isDarwinHandlerInstalled() bool { |
|
|
appPath := getDarwinAppPath() |
|
|
_, err := os.Stat(appPath) |
|
|
return err == nil |
|
|
} |
|
|
|
|
|
func installDarwinHandler(handlerPort int) error { |
|
|
|
|
|
appPath := getDarwinAppPath() |
|
|
contentsPath := filepath.Join(appPath, "Contents") |
|
|
macOSPath := filepath.Join(contentsPath, "MacOS") |
|
|
|
|
|
if err := os.MkdirAll(macOSPath, 0755); err != nil { |
|
|
return fmt.Errorf("failed to create app bundle: %w", err) |
|
|
} |
|
|
|
|
|
|
|
|
plistPath := filepath.Join(contentsPath, "Info.plist") |
|
|
plistContent := `<?xml version="1.0" encoding="UTF-8"?> |
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|
|
<plist version="1.0"> |
|
|
<dict> |
|
|
<key>CFBundleIdentifier</key> |
|
|
<string>com.cliproxyapi.kiro-oauth-handler</string> |
|
|
<key>CFBundleName</key> |
|
|
<string>KiroOAuthHandler</string> |
|
|
<key>CFBundleExecutable</key> |
|
|
<string>kiro-oauth-handler</string> |
|
|
<key>CFBundleVersion</key> |
|
|
<string>1.0</string> |
|
|
<key>CFBundleURLTypes</key> |
|
|
<array> |
|
|
<dict> |
|
|
<key>CFBundleURLName</key> |
|
|
<string>Kiro Protocol</string> |
|
|
<key>CFBundleURLSchemes</key> |
|
|
<array> |
|
|
<string>kiro</string> |
|
|
</array> |
|
|
</dict> |
|
|
</array> |
|
|
<key>LSBackgroundOnly</key> |
|
|
<true/> |
|
|
</dict> |
|
|
</plist>` |
|
|
|
|
|
if err := os.WriteFile(plistPath, []byte(plistContent), 0644); err != nil { |
|
|
return fmt.Errorf("failed to write Info.plist: %w", err) |
|
|
} |
|
|
|
|
|
|
|
|
execPath := filepath.Join(macOSPath, "kiro-oauth-handler") |
|
|
execContent := fmt.Sprintf(`#!/bin/bash |
|
|
# Kiro OAuth Protocol Handler for macOS |
|
|
|
|
|
URL="$1" |
|
|
|
|
|
# Check curl availability (should always exist on macOS) |
|
|
if [ ! -x /usr/bin/curl ]; then |
|
|
echo "Error: curl is required for Kiro OAuth handler" >&2 |
|
|
exit 1 |
|
|
fi |
|
|
|
|
|
# Extract code and state from URL |
|
|
[[ "$URL" =~ code=([^&]+) ]] && CODE="${BASH_REMATCH[1]}" |
|
|
[[ "$URL" =~ state=([^&]+) ]] && STATE="${BASH_REMATCH[1]}" |
|
|
[[ "$URL" =~ error=([^&]+) ]] && ERROR="${BASH_REMATCH[1]}" |
|
|
|
|
|
# Try multiple ports (default + dynamic range) |
|
|
for PORT in %d %d %d %d %d; do |
|
|
if [ -n "$ERROR" ]; then |
|
|
/usr/bin/curl -sf --connect-timeout 1 "http://127.0.0.1:$PORT/oauth/callback?error=$ERROR" && exit 0 |
|
|
elif [ -n "$CODE" ] && [ -n "$STATE" ]; then |
|
|
/usr/bin/curl -sf --connect-timeout 1 "http://127.0.0.1:$PORT/oauth/callback?code=$CODE&state=$STATE" && exit 0 |
|
|
fi |
|
|
done |
|
|
`, handlerPort, handlerPort+1, handlerPort+2, handlerPort+3, handlerPort+4) |
|
|
|
|
|
if err := os.WriteFile(execPath, []byte(execContent), 0755); err != nil { |
|
|
return fmt.Errorf("failed to write executable: %w", err) |
|
|
} |
|
|
|
|
|
|
|
|
cmd := exec.Command("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister", |
|
|
"-f", appPath) |
|
|
if err := cmd.Run(); err != nil { |
|
|
log.Warnf("lsregister failed (handler may still work): %v", err) |
|
|
} |
|
|
|
|
|
log.Info("Kiro protocol handler installed for macOS") |
|
|
return nil |
|
|
} |
|
|
|
|
|
func uninstallDarwinHandler() error { |
|
|
appPath := getDarwinAppPath() |
|
|
|
|
|
|
|
|
cmd := exec.Command("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister", |
|
|
"-u", appPath) |
|
|
_ = cmd.Run() |
|
|
|
|
|
|
|
|
if err := os.RemoveAll(appPath); err != nil && !os.IsNotExist(err) { |
|
|
return fmt.Errorf("failed to remove app bundle: %w", err) |
|
|
} |
|
|
|
|
|
log.Info("Kiro protocol handler uninstalled") |
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
func ParseKiroURI(rawURI string) (*AuthCallback, error) { |
|
|
u, err := url.Parse(rawURI) |
|
|
if err != nil { |
|
|
return nil, fmt.Errorf("invalid URI: %w", err) |
|
|
} |
|
|
|
|
|
if u.Scheme != KiroProtocol { |
|
|
return nil, fmt.Errorf("invalid scheme: expected %s, got %s", KiroProtocol, u.Scheme) |
|
|
} |
|
|
|
|
|
if u.Host != KiroAuthority { |
|
|
return nil, fmt.Errorf("invalid authority: expected %s, got %s", KiroAuthority, u.Host) |
|
|
} |
|
|
|
|
|
query := u.Query() |
|
|
return &AuthCallback{ |
|
|
Code: query.Get("code"), |
|
|
State: query.Get("state"), |
|
|
Error: query.Get("error"), |
|
|
}, nil |
|
|
} |
|
|
|
|
|
|
|
|
func GetHandlerInstructions() string { |
|
|
switch runtime.GOOS { |
|
|
case "linux": |
|
|
return `To manually set up the Kiro protocol handler on Linux: |
|
|
|
|
|
1. Create ~/.local/share/applications/kiro-oauth-handler.desktop: |
|
|
[Desktop Entry] |
|
|
Name=Kiro OAuth Handler |
|
|
Exec=~/.local/bin/kiro-oauth-handler %u |
|
|
Type=Application |
|
|
Terminal=false |
|
|
MimeType=x-scheme-handler/kiro; |
|
|
|
|
|
2. Create ~/.local/bin/kiro-oauth-handler (make it executable): |
|
|
#!/bin/bash |
|
|
URL="$1" |
|
|
# ... (see generated script for full content) |
|
|
|
|
|
3. Run: xdg-mime default kiro-oauth-handler.desktop x-scheme-handler/kiro` |
|
|
|
|
|
case "windows": |
|
|
return `To manually set up the Kiro protocol handler on Windows: |
|
|
|
|
|
1. Open Registry Editor (regedit.exe) |
|
|
2. Create key: HKEY_CURRENT_USER\Software\Classes\kiro |
|
|
3. Set default value to: URL:Kiro Protocol |
|
|
4. Create string value "URL Protocol" with empty data |
|
|
5. Create subkey: shell\open\command |
|
|
6. Set default value to: "C:\path\to\handler.bat" "%1"` |
|
|
|
|
|
case "darwin": |
|
|
return `To manually set up the Kiro protocol handler on macOS: |
|
|
|
|
|
1. Create ~/Applications/KiroOAuthHandler.app bundle |
|
|
2. Add Info.plist with CFBundleURLTypes containing "kiro" scheme |
|
|
3. Create executable in Contents/MacOS/ |
|
|
4. Run: /System/Library/.../lsregister -f ~/Applications/KiroOAuthHandler.app` |
|
|
|
|
|
default: |
|
|
return "Protocol handler setup is not supported on this platform." |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func SetupProtocolHandlerIfNeeded(handlerPort int) error { |
|
|
if IsProtocolHandlerInstalled() { |
|
|
log.Debug("Kiro protocol handler already installed") |
|
|
return nil |
|
|
} |
|
|
|
|
|
fmt.Println("\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ") |
|
|
fmt.Println("β Kiro Protocol Handler Setup Required β") |
|
|
fmt.Println("ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ") |
|
|
fmt.Println("\nTo enable Google/GitHub login, we need to install a protocol handler.") |
|
|
fmt.Println("This allows your browser to redirect back to the CLI after authentication.") |
|
|
fmt.Println("\nInstalling protocol handler...") |
|
|
|
|
|
if err := InstallProtocolHandler(handlerPort); err != nil { |
|
|
fmt.Printf("\nβ Automatic installation failed: %v\n", err) |
|
|
fmt.Println("\nManual setup instructions:") |
|
|
fmt.Println(strings.Repeat("-", 60)) |
|
|
fmt.Println(GetHandlerInstructions()) |
|
|
return err |
|
|
} |
|
|
|
|
|
fmt.Println("\nβ Protocol handler installed successfully!") |
|
|
return nil |
|
|
} |
|
|
|