Skip to content

Commit ba32144

Browse files
committed
fix: install + whoami bugs (#68, #56, #41)
- install.sh: detect root with `id -u` instead of the bash-only `$EUID`, so `curl ... | sh` under a POSIX shell (dash) installs to /usr/local/bin as root instead of silently falling back to ~/.local/bin (#68). - npm/install.js: extract the Windows .zip with PowerShell's Expand-Archive instead of the Unix `unzip` command, fixing `npm install -g` on Windows (#56). - whoami/user: request `verified_type` and `subscription_type` so Premium/blue accounts report correctly instead of `verified: false` (#41).
1 parent 06f7426 commit ba32144

5 files changed

Lines changed: 17 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ All user-visible bugs and enhancements should be recorded here.
66

77
### Fixed
88

9+
- [2026-06-29] `install.sh` now uses `id -u` instead of the bash-only `$EUID` to detect root, so `curl ... | sh` (POSIX/dash) installs to `/usr/local/bin` as root instead of silently falling back to `~/.local/bin`. (#68)
10+
- [2026-06-29] npm install on Windows works again: `install.js` extracts the `.zip` with PowerShell's `Expand-Archive` instead of the Unix `unzip` command. (#56)
11+
- [2026-06-29] `whoami` (and `user`) now request `verified_type` and `subscription_type`, so Premium/blue accounts are reported correctly instead of `verified: false`. (#41)
912
- [2026-06-29] OAuth2 token exchange and refresh now send client credentials with the correct auth style — HTTP Basic header for confidential clients (those with a secret), `client_id` in the body for public clients — instead of relying on autodetection, which could fail against X with `unauthorized_client: Missing valid authorization header`.
1013
- [2026-06-29] `mcp` bridge no longer launches a browser at startup: it still refreshes an existing token silently, but when none is available it fails fast with instructions (`xurl auth oauth2 [--app NAME] [--headless]`) instead of opening a browser mid-startup (which could hang an MCP client's handshake) and printing to the JSON-RPC stdout channel. OAuth2 diagnostics now go to stderr.
1114
- [2026-06-29] `mcp` bridge no longer lets a strict client hang: a request that cannot be answered — transport failure, a failed token refresh/retry after a 401, or a response with an empty/non-JSON body — now gets a synthesized JSON-RPC error keyed to its id. Notifications (e.g. `notifications/cancelled`) are no longer head-of-line blocked behind an in-flight streaming response, large but valid JSON error bodies are forwarded whole instead of being truncated, the standalone server->client stream stops probing a non-event-stream `200` and only resets its reconnect backoff after a healthy stream, and stdin memory stays bounded when an oversized line is dropped.

api/shortcuts.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ func SearchPosts(client Client, query string, maxResults int, opts RequestOption
187187
// GetMe fetches the authenticated user's profile.
188188
func GetMe(client Client, opts RequestOptions) (json.RawMessage, error) {
189189
opts.Method = "GET"
190-
opts.Endpoint = "/2/users/me?user.fields=created_at,description,public_metrics,verified,profile_image_url"
190+
opts.Endpoint = "/2/users/me?user.fields=created_at,description,public_metrics,verified,verified_type,subscription_type,profile_image_url"
191191
opts.Data = ""
192192

193193
return client.SendRequest(opts)
@@ -198,7 +198,7 @@ func LookupUser(client Client, username string, opts RequestOptions) (json.RawMe
198198
username = ResolveUsername(username)
199199

200200
opts.Method = "GET"
201-
opts.Endpoint = fmt.Sprintf("/2/users/by/username/%s?user.fields=created_at,description,public_metrics,verified,profile_image_url", username)
201+
opts.Endpoint = fmt.Sprintf("/2/users/by/username/%s?user.fields=created_at,description,public_metrics,verified,verified_type,subscription_type,profile_image_url", username)
202202
opts.Data = ""
203203

204204
return client.SendRequest(opts)

cli/shortcuts_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func (f fakeClient) SendMultipartRequest(options api.MultipartOptions) (json.Raw
3939
func TestResolveMyUserIDUsesUsernameFallback(t *testing.T) {
4040
client := fakeClient{
4141
sendRequest: func(options api.RequestOptions) (json.RawMessage, error) {
42-
require.Equal(t, "/2/users/by/username/alice?user.fields=created_at,description,public_metrics,verified,profile_image_url", options.Endpoint)
42+
require.Equal(t, "/2/users/by/username/alice?user.fields=created_at,description,public_metrics,verified,verified_type,subscription_type,profile_image_url", options.Endpoint)
4343
return json.RawMessage(`{"data":{"id":"42"}}`), nil
4444
},
4545
}
@@ -52,7 +52,7 @@ func TestResolveMyUserIDUsesUsernameFallback(t *testing.T) {
5252
func TestResolveMyUserIDReturnsHelpfulErrorWhenGetMeFails(t *testing.T) {
5353
client := fakeClient{
5454
sendRequest: func(options api.RequestOptions) (json.RawMessage, error) {
55-
require.Equal(t, "/2/users/me?user.fields=created_at,description,public_metrics,verified,profile_image_url", options.Endpoint)
55+
require.Equal(t, "/2/users/me?user.fields=created_at,description,public_metrics,verified,verified_type,subscription_type,profile_image_url", options.Endpoint)
5656
return nil, fmt.Errorf("boom")
5757
},
5858
}

install.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ PROGRAM_NAME="xurl"
77

88
# Install to ~/.local/bin by default (no sudo needed).
99
# Falls back to /usr/local/bin if run as root.
10-
if [ "$EUID" -eq 0 ]; then
10+
# Use `id -u` rather than `$EUID`: $EUID is bash-only and expands to empty under
11+
# POSIX shells (e.g. dash, the default /bin/sh on Debian/Ubuntu), which silently
12+
# picks the user path even when run as root via `curl ... | sh`.
13+
if [ "$(id -u)" -eq 0 ]; then
1114
INSTALL_DIR="/usr/local/bin"
1215
else
1316
INSTALL_DIR="${HOME}/.local/bin"

npm/install.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,12 @@ async function install() {
6969
if (ext === "tar.gz") {
7070
execSync(`tar xzf "${archivePath}" -C "${tmpDir}"`);
7171
} else {
72-
execSync(`unzip -o "${archivePath}" -d "${tmpDir}"`);
72+
// Windows releases are .zip. Extract with PowerShell's Expand-Archive,
73+
// which is built into Windows, instead of the Unix `unzip` command (which
74+
// isn't present on Windows and broke `npm install -g` there).
75+
execSync(
76+
`powershell -NoProfile -NonInteractive -Command "Expand-Archive -LiteralPath '${archivePath}' -DestinationPath '${tmpDir}' -Force"`
77+
);
7378
}
7479

7580
const binaryName = plat === "Windows" ? "xurl.exe" : "xurl";

0 commit comments

Comments
 (0)