diff --git a/README.md b/README.md index 2d30cf5..327bd51 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,35 @@ make build ## Setup -1. Create a Basecamp OAuth app at https://launchpad.37signals.com/integrations -2. Run `basecamp init` to configure credentials -3. Run `basecamp auth` to authenticate +### Prerequisites -Configuration files (XDG Base Directory): +During OAuth authentication, Basecamp redirects to your computer on **port 3002**. Your machine must be accessible via a URL for this to work. Use a service like: + +- [Tailscale](https://tailscale.com/) - Recommended for persistent access +- [ngrok](https://ngrok.com/) - Quick setup for temporary access +- Any reverse proxy that exposes localhost:3002 + +### Registration + +1. Start your tunnel service and note the public URL (e.g., `https://myhost.tailscale.ts.net`) + +2. Run the registration helper to generate your OAuth app values: + +```bash +basecamp register +``` + +This will ask for your application details and output the exact values to enter in the Basecamp registration form, including the correct redirect URI. + +3. Visit https://launchpad.37signals.com/integrations and register your app using the generated values + +4. Run `basecamp init` to configure your credentials (Client ID, Client Secret, and the same Redirect URI) + +5. Run `basecamp auth` to authenticate (ensure your tunnel is running on port 3002) + +### Configuration Files + +Configuration follows XDG Base Directory specification: - `~/.config/basecamp/config.json` - client credentials - `~/.local/share/basecamp/token.json` - OAuth token @@ -350,6 +374,14 @@ basecamp card 44444444 # just need card_id The CLI searches current directory and parent directories for `.basecamp.yml`. +## Agent Skills + +Install the Basecamp skill for AI coding agents (Claude Code, OpenCode, and others): + +```bash +npx skills add robzolkos/basecamp-cli +``` + ## Output All commands output JSON for easy parsing with `jq`: diff --git a/internal/commands/register.go b/internal/commands/register.go new file mode 100644 index 0000000..18afa16 --- /dev/null +++ b/internal/commands/register.go @@ -0,0 +1,73 @@ +package commands + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +type RegisterCmd struct{} + +func (c *RegisterCmd) Run(args []string) error { + fmt.Fprintln(os.Stderr, "Basecamp OAuth App Registration Helper") + fmt.Fprintln(os.Stderr, strings.Repeat("=", 40)) + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, "This helper will generate the values you need to register") + fmt.Fprintln(os.Stderr, "your Basecamp OAuth application.") + fmt.Fprintln(os.Stderr) + + reader := bufio.NewReader(os.Stdin) + + appName := prompt(reader, "Application name", "My Basecamp CLI") + companyName := prompt(reader, "Company/Organization name", "") + websiteURL := prompt(reader, "Website URL", "https://github.com/robzolkos/basecamp-cli") + accessibleURL := prompt(reader, "URL where this computer is accessible (e.g., https://myhost.tailscale.ts.net)", "") + + // Build redirect URI from accessible URL + redirectURI := buildRedirectURI(accessibleURL) + + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, strings.Repeat("=", 60)) + fmt.Fprintln(os.Stderr, "REGISTRATION INSTRUCTIONS") + fmt.Fprintln(os.Stderr, strings.Repeat("=", 60)) + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, "1. Visit: https://launchpad.37signals.com/integrations") + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, "2. Click 'Register another application'") + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, "3. Fill out the form with these values:") + fmt.Fprintln(os.Stderr) + fmt.Fprintf(os.Stderr, " Name of your application: %s\n", appName) + fmt.Fprintf(os.Stderr, " Your company/organization: %s\n", companyName) + fmt.Fprintf(os.Stderr, " Website URL: %s\n", websiteURL) + fmt.Fprintf(os.Stderr, " Redirect URI: %s\n", redirectURI) + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, "4. After registering, copy your Client ID and Client Secret") + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, "5. Run 'basecamp init' and enter the credentials when prompted") + fmt.Fprintln(os.Stderr, " (use the same Redirect URI shown above)") + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, "6. Run 'basecamp auth' to authenticate") + fmt.Fprintln(os.Stderr, strings.Repeat("=", 60)) + + return PrintJSON(map[string]string{ + "application_name": appName, + "company_name": companyName, + "website_url": websiteURL, + "redirect_uri": redirectURI, + "registration_url": "https://launchpad.37signals.com/integrations", + }) +} + +func buildRedirectURI(accessibleURL string) string { + if accessibleURL == "" { + return "http://localhost:3002/callback" + } + + // Remove trailing slash if present + accessibleURL = strings.TrimSuffix(accessibleURL, "/") + + // Add port and callback path + return accessibleURL + ":3002/callback" +} diff --git a/internal/commands/root.go b/internal/commands/root.go index 06a7d3d..e2e10be 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -14,6 +14,7 @@ type Command interface { } var commands = map[string]func() Command{ + "register": func() Command { return &RegisterCmd{} }, "init": func() Command { return &InitCmd{} }, "auth": func() Command { return &AuthCmd{} }, "projects": func() Command { return &ProjectsCmd{} }, @@ -115,6 +116,7 @@ func printHelp(version string) { Usage: basecamp [arguments] [flags] Commands: + register Generate OAuth app registration values init Configure credentials auth Authenticate with OAuth projects List all projects