Skip to content

Commit 8371a07

Browse files
committed
feat: multi-app support, YAML config, Bubble Tea picker, npm/brew distribution
- Multiple X API apps with isolated credentials and tokens - Interactive default app/user picker (Bubble Tea) - Single-command defaults: xurl auth default <app> <user> - Per-request --app override - YAML .xurl format with auto-migration from legacy JSON - Credential backfill from env vars into migrated apps - auth apps add/update/remove/list commands - auth status shows per-app breakdown - Homebrew tap via goreleaser - npm package @xdevplatform/xurl - Install script no longer requires sudo - Updated README and SKILL.md
1 parent 09a173c commit 8371a07

19 files changed

Lines changed: 1834 additions & 186 deletions

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Set up Go
1717
uses: actions/setup-go@v4
1818
with:
19-
go-version: '1.20'
19+
go-version: '1.24'
2020

2121
- name: Build
2222
run: go build -v ./...

.github/workflows/release.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
19+
- name: Set up Go
20+
uses: actions/setup-go@v4
21+
with:
22+
go-version: "1.24"
23+
24+
- name: Run tests
25+
run: go test ./...
26+
27+
- name: Run GoReleaser
28+
uses: goreleaser/goreleaser-action@v6
29+
with:
30+
version: "~> v2"
31+
args: release --clean
32+
env:
33+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.goreleaser.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ changelog:
5050
- "^docs:"
5151
- "^test:"
5252

53+
brews:
54+
- repository:
55+
owner: xdevplatform
56+
name: homebrew-tap
57+
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
58+
directory: Formula
59+
homepage: "https://github.com/xdevplatform/xurl"
60+
description: "Auth-enabled curl-like CLI for the X API"
61+
license: "MIT"
62+
install: |
63+
bin.install "xurl"
64+
test: |
65+
system "#{bin}/xurl", "version"
66+
5367
release:
5468
footer: >-
5569

README.md

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,36 @@ A command-line tool for interacting with the X (formerly Twitter) API, supportin
44

55
## Features
66

7+
- **Multi-app support** — register multiple X API apps with separate credentials and tokens
78
- OAuth 2.0 PKCE flow authentication
89
- OAuth 1.0a authentication
9-
- Multiple OAuth 2.0 account support
10-
- Persistent token storage
10+
- Multiple OAuth 2.0 account support per app
11+
- Default app and default user selection (interactive Bubble Tea picker or single command)
12+
- Persistent token storage in YAML (`~/.xurl`), auto-migrates from legacy JSON
1113
- HTTP request customization (headers, methods, body)
14+
- Per-request app override with `--app`
1215

1316
## Installation
17+
18+
### Homebrew (macOS / Linux)
19+
```bash
20+
brew install xdevplatform/tap/xurl
21+
```
22+
23+
### npm
24+
```bash
25+
npm install -g @xdevplatform/xurl
26+
```
27+
28+
### Shell script (no sudo required)
29+
```bash
30+
curl -fsSL https://raw.githubusercontent.com/xdevplatform/xurl/main/install.sh | bash
31+
```
32+
Installs to `~/.local/bin`. If it's not in your PATH, the script will tell you what to add.
33+
34+
### Go
1435
```bash
15-
curl -fsSL https://raw.githubusercontent.com/xdevplatform/xurl/main/install.sh | sudo bash
36+
go install github.com/xdevplatform/xurl@latest
1637
```
1738

1839

@@ -22,39 +43,104 @@ curl -fsSL https://raw.githubusercontent.com/xdevplatform/xurl/main/install.sh |
2243

2344
You must have a developer account and app to use this tool.
2445

25-
#### App authentication:
46+
#### Register an app
47+
48+
Register your X API app credentials so they're stored in `~/.xurl` (no env vars needed after this):
49+
2650
```bash
27-
xurl auth app --bearer-token BEARER_TOKEN
51+
xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
52+
```
53+
54+
You can register multiple apps:
55+
```bash
56+
xurl auth apps add prod-app --client-id PROD_ID --client-secret PROD_SECRET
57+
xurl auth apps add dev-app --client-id DEV_ID --client-secret DEV_SECRET
2858
```
2959

60+
> **Legacy / env-var flow:** You can also set `CLIENT_ID` and `CLIENT_SECRET` as environment variables. They'll be auto-saved into the active app on first use.
61+
3062
#### OAuth 2.0 User-Context
3163
**Note:** For OAuth 2.0 authentication, you must specify the redirect URI in the [X API developer portal](https://developer.x.com/en/portal/dashboard).
3264

3365
1. Create an app at the [X API developer portal](https://developer.x.com/en/portal/dashboard).
3466
2. Go to authentication settings and set the redirect URI to `http://localhost:8080/callback`.
3567
![Setup](./assets/setup.png)
3668
![Redirect URI](./assets/callback.png)
37-
3. Set the client ID and secret in your environment variables.
38-
```env
39-
export CLIENT_ID=your_client_id
40-
export CLIENT_SECRET=your_client_secret
69+
3. Register the app (if you haven't already):
70+
```bash
71+
xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
4172
```
4273
4. Get your access keys:
4374
```bash
4475
xurl auth oauth2
4576
```
77+
78+
#### App authentication (bearer token):
79+
```bash
80+
xurl auth app --bearer-token BEARER_TOKEN
81+
```
82+
4683
#### OAuth 1.0a authentication:
4784
```bash
4885
xurl auth oauth1 --consumer-key KEY --consumer-secret SECRET --access-token TOKEN --token-secret SECRET
4986
```
5087

51-
### Authentication Management
52-
View authentication status:
88+
### Multi-App Management
89+
90+
List registered apps:
91+
```bash
92+
xurl auth apps list
93+
```
94+
95+
Update credentials on an existing app:
96+
```bash
97+
xurl auth apps update my-app --client-id NEW_ID --client-secret NEW_SECRET
98+
```
99+
100+
Remove an app:
101+
```bash
102+
xurl auth apps remove old-app
103+
```
104+
105+
Set the default app and user — **interactive picker** (uses Bubble Tea):
106+
```bash
107+
xurl auth default
108+
```
109+
110+
Set the default app and user — **single command**:
111+
```bash
112+
xurl auth default my-app # set default app
113+
xurl auth default my-app alice # set default app + default user
114+
```
115+
116+
Use a specific app for a single request:
117+
```bash
118+
xurl --app dev-app /2/users/me
119+
```
120+
121+
### Authentication Status
122+
View authentication status across all apps:
53123
```bash
54124
xurl auth status
55125
```
56126

57-
Clear authentication:
127+
Example output:
128+
```
129+
▸ my-app [client_id: VUttdG9P…]
130+
▸ oauth2: alice
131+
oauth2: bob
132+
oauth1: ✓
133+
bearer: ✓
134+
135+
dev-app [client_id: OTHER789…]
136+
oauth2: (none)
137+
oauth1: –
138+
bearer: –
139+
```
140+
141+
`` on the left = default app. `` next to a user = default user.
142+
143+
### Clear Authentication
58144
```bash
59145
xurl auth clear --all # Clear all tokens
60146
xurl auth clear --oauth1 # Clear OAuth 1.0a tokens
@@ -190,7 +276,28 @@ xurl '/2/media/upload?command=STATUS&media_id=MEDIA_ID'
190276
191277
## Token Storage
192278
193-
Tokens are stored securely in `~/.xurl` in your home directory.
279+
Tokens and app credentials are stored in `~/.xurl` in YAML format. Each registered app has its own isolated set of tokens. Example:
280+
281+
```yaml
282+
apps:
283+
my-app:
284+
client_id: abc123
285+
client_secret: secret456
286+
default_user: alice
287+
oauth2_tokens:
288+
alice:
289+
type: oauth2
290+
oauth2:
291+
access_token: "..."
292+
refresh_token: "..."
293+
expiration_time: 1234567890
294+
bearer_token:
295+
type: bearer
296+
bearer: "AAAA..."
297+
default_app: my-app
298+
```
299+
300+
> **Migration:** If you have an existing JSON-format `~/.xurl` file from a previous version, it will be automatically migrated to the new YAML multi-app format on first use. Your tokens are preserved in a `default` app.
194301
195302
## Contributing
196303
Contributions are welcome!

SKILL.md

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: xurl
3-
description: A curl-like CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, upload media, or interact with any X API v2 endpoint. Supports OAuth 2.0, OAuth 1.0a, and app-only auth.
3+
description: A curl-like CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, upload media, or interact with any X API v2 endpoint. Supports multiple apps, OAuth 2.0, OAuth 1.0a, and app-only auth.
44
---
55

66
# xurl — Agent Skill Reference
@@ -11,24 +11,40 @@ description: A curl-like CLI tool for making authenticated requests to the X (Tw
1111

1212
## Prerequisites
1313

14-
Before using any command you must be authenticated. Run `xurl auth status` to check. If not authenticated, set up one of:
14+
Before using any command you must be authenticated. Run `xurl auth status` to check.
15+
16+
### Register an app (recommended)
1517

1618
```bash
17-
# Option 1 — OAuth 2.0 (user‑context, most common)
18-
export CLIENT_ID=your_client_id
19-
export CLIENT_SECRET=your_client_secret
19+
# Register your X API app credentials (stored in ~/.xurl)
20+
xurl auth apps add my-app --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
21+
22+
# Then authenticate
2023
xurl auth oauth2
24+
```
25+
26+
You can register multiple apps and switch between them:
27+
```bash
28+
xurl auth apps add prod-app --client-id PROD_ID --client-secret PROD_SECRET
29+
xurl auth apps add dev-app --client-id DEV_ID --client-secret DEV_SECRET
30+
xurl auth default prod-app # set default app
31+
xurl auth default prod-app alice # set default app + user
32+
xurl --app dev-app /2/users/me # one-off override
33+
```
34+
35+
### Other auth methods
2136

22-
# Option 2 — OAuth 1.0a
37+
```bash
38+
# OAuth 1.0a
2339
xurl auth oauth1 \
2440
--consumer-key KEY --consumer-secret SECRET \
2541
--access-token TOKEN --token-secret SECRET
2642

27-
# Option 3 — App‑only bearer token
43+
# App‑only bearer token
2844
xurl auth app --bearer-token TOKEN
2945
```
3046

31-
Tokens are persisted to `~/.xurl`. Once authenticated, every command below will auto‑attach the right `Authorization` header.
47+
Tokens are persisted to `~/.xurl` in YAML format. Each app has its own isolated tokens. Once authenticated, every command below will auto‑attach the right `Authorization` header.
3248

3349
---
3450

@@ -66,6 +82,15 @@ Tokens are persisted to `~/.xurl`. Once authenticated, every command below will
6682
| List DMs | `xurl dms -n 10` |
6783
| Upload media | `xurl media upload path/to/file.mp4` |
6884
| Media status | `xurl media status MEDIA_ID` |
85+
| **App Management** | |
86+
| Register app | `xurl auth apps add NAME --client-id ID --client-secret SEC` |
87+
| List apps | `xurl auth apps list` |
88+
| Update app creds | `xurl auth apps update NAME --client-id ID` |
89+
| Remove app | `xurl auth apps remove NAME` |
90+
| Set default (interactive) | `xurl auth default` |
91+
| Set default (command) | `xurl auth default APP_NAME [USERNAME]` |
92+
| Use app per-request | `xurl --app NAME /2/users/me` |
93+
| Auth status | `xurl auth status` |
6994

7095
> **Post IDs vs URLs:** Anywhere `POST_ID` appears above you can also paste a full post URL (e.g. `https://x.com/user/status/1234567890`) — xurl extracts the ID automatically.
7196
@@ -216,10 +241,11 @@ xurl post "lol" --media-id MEDIA_ID
216241

217242
## Global Flags
218243

219-
These flags work on every shortcut command:
244+
These flags work on every command:
220245

221246
| Flag | Short | Description |
222247
|---|---|---|
248+
| `--app` | | Use a specific registered app for this request (overrides default) |
223249
| `--auth` | | Force auth type: `oauth1`, `oauth2`, or `app` |
224250
| `--username` | `-u` | Which OAuth2 account to use (if you have multiple) |
225251
| `--verbose` | `-v` | Print full request/response headers |
@@ -332,6 +358,24 @@ xurl mentions -n 20
332358
xurl timeline -n 20
333359
```
334360

361+
### Set up multiple apps
362+
```bash
363+
# Register two apps
364+
xurl auth apps add prod --client-id PROD_ID --client-secret PROD_SECRET
365+
xurl auth apps add staging --client-id STG_ID --client-secret STG_SECRET
366+
367+
# Authenticate users on each
368+
xurl auth default prod
369+
xurl auth oauth2 # authenticates on prod app
370+
371+
xurl auth default staging
372+
xurl auth oauth2 # authenticates on staging app
373+
374+
# Switch between them
375+
xurl auth default prod alice # prod app, alice user
376+
xurl --app staging /2/users/me # one-off request against staging
377+
```
378+
335379
---
336380

337381
## Error Handling
@@ -348,4 +392,7 @@ xurl timeline -n 20
348392
- **Rate limits:** The X API enforces rate limits per endpoint. If you get a 429 error, wait and retry. Write endpoints (post, reply, like, repost) have stricter limits than read endpoints.
349393
- **Scopes:** OAuth 2.0 tokens are requested with broad scopes. If you get a 403 on a specific action, your token may lack the required scope — re‑run `xurl auth oauth2` to get a fresh token.
350394
- **Token refresh:** OAuth 2.0 tokens auto‑refresh when expired. No manual intervention needed.
351-
- **Multiple accounts:** You can authenticate multiple OAuth 2.0 accounts and switch between them with `--username` / `-u`.
395+
- **Multiple apps:** Register multiple apps with `xurl auth apps add`. Each app has its own isolated credentials and tokens. Switch with `xurl auth default` or `--app`.
396+
- **Multiple accounts:** You can authenticate multiple OAuth 2.0 accounts per app and switch between them with `--username` / `-u` or set a default with `xurl auth default APP USER`.
397+
- **Default user:** When no `-u` flag is given, xurl uses the default user for the active app (set via `xurl auth default`). If no default user is set, it uses the first available token.
398+
- **Token storage:** `~/.xurl` is YAML. Each app stores its own credentials and tokens.

api/client_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@ func createTempTokenStore(t *testing.T) (*store.TokenStore, string) {
2525
t.Fatalf("Failed to create temp directory: %v", err)
2626
}
2727

28-
tempFile := filepath.Join(tempDir, "tokens.json")
28+
tempFile := filepath.Join(tempDir, ".xurl")
2929
tokenStore := &store.TokenStore{
30+
Apps: make(map[string]*store.App),
31+
DefaultApp: "default",
32+
FilePath: tempFile,
33+
}
34+
tokenStore.Apps["default"] = &store.App{
3035
OAuth2Tokens: make(map[string]store.Token),
31-
FilePath: tempFile,
3236
}
3337

3438
return tokenStore, tempDir

0 commit comments

Comments
 (0)