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
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`:
Expand Down
73 changes: 73 additions & 0 deletions internal/commands/register.go
Original file line number Diff line number Diff line change
@@ -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"
}
2 changes: 2 additions & 0 deletions internal/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{} },
Expand Down Expand Up @@ -115,6 +116,7 @@ func printHelp(version string) {
Usage: basecamp <command> [arguments] [flags]

Commands:
register Generate OAuth app registration values
init Configure credentials
auth Authenticate with OAuth
projects List all projects
Expand Down
Loading