From 50836a9f17945e2cfa4e4504c4ec9e688c87efe4 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Fri, 30 Jan 2026 18:37:28 -0500 Subject: [PATCH] Add register helper command for OAuth app setup Adds a new `basecamp register` command that interactively collects application details and outputs the exact values needed for Basecamp OAuth app registration. Also updates README with clearer setup instructions explaining the port 3002 requirement and tunnel services (Tailscale, ngrok) needed for OAuth callback. --- README.md | 40 +++++++++++++++++-- internal/commands/register.go | 73 +++++++++++++++++++++++++++++++++++ internal/commands/root.go | 2 + 3 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 internal/commands/register.go 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