Skip to content

Commit 29ce49d

Browse files
authored
refactor(util): share a browser opener across cli, tui, and core (anomalyco#51412)
1 parent 00738c5 commit 29ce49d

16 files changed

Lines changed: 89 additions & 39 deletions

File tree

‎bun.lock‎

Lines changed: 34 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
"@types/node": "catalog:",
163163
"bun-types": "1.4.2",
164164
"effect": "catalog:",
165+
"open": "11.0.4",
165166
"solid-js": "catalog:"
166167
},
167168
"patchedDependencies": {

‎packages/cli/package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
"effect": "catalog:",
4242
"immer": "11.1.4",
4343
"jsonc-parser": "3.3.1",
44-
"open": "10.1.2",
4544
"solid-js": "catalog:",
4645
"tree-sitter-bash": "0.25.0",
4746
"tree-sitter-powershell": "0.25.10",

‎packages/cli/src/ui/prompt.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export function requireInteractive(message: string) {
1616
}
1717

1818
export const openUrl = Effect.fn("cli.prompt.open-url")(function* (url: string) {
19-
const { default: open } = yield* Effect.promise(() => import("open"))
20-
yield* Effect.promise(() => open(url)).pipe(Effect.ignore)
19+
const browser = yield* Effect.promise(() => import("@opencode/util/open"))
20+
yield* Effect.promise(() => browser.openUrl(url)).pipe(Effect.ignore)
2121
})
2222

2323
export function handlePromptErrors<A, E, R>(effect: Effect.Effect<A, E, R>) {

‎packages/core/src/mcp/oauth.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ export const provider = (options: Options): OAuthClientProvider => {
197197
saveTokens: (tokens) => options.store.saveTokens(tokens),
198198
redirectToAuthorization: (url) => {
199199
if (!redirect) throw refuse("user authorization")
200+
if (url.protocol !== "http:" && url.protocol !== "https:")
201+
throw new Error(`MCP server "${options.config.url}" returned a ${url.protocol} authorization URL; only http and https are supported`)
200202
return redirect.open(url)
201203
},
202204
...(options.invalidate ? { invalidateCredentials: options.invalidate } : {}),

‎packages/core/test/mcp-oauth.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,14 @@ describe("MCP OAuth", () => {
291291
await expect(authorize("not a URL")).rejects.toThrow(TypeError)
292292
})
293293

294+
test("rejects an authorization endpoint that is not http or https", async () => {
295+
const { server } = authorizationServer({ authorization_endpoint: "file:///tmp/authorize" })
296+
297+
await expect(Effect.runPromise(Effect.scoped(start(server))).finally(() => server.stop(true))).rejects.toThrow(
298+
"returned a file: authorization URL",
299+
)
300+
})
301+
294302
test("sends the configured URL as the resource when the server publishes no metadata", async () => {
295303
const { server, tokenRequests } = authorizationServer({})
296304
const url = `${server.url.origin}/mcp`

‎packages/tui/package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@
9494
"effect": "catalog:",
9595
"fuzzysort": "catalog:",
9696
"get-east-asian-width": "catalog:",
97-
"open": "10.1.2",
9897
"opentui-spinner": "catalog:",
9998
"remeda": "catalog:",
10099
"solid-js": "catalog:",

‎packages/tui/src/app.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ import { Toast, ToastProvider, useToast } from "./ui/toast"
8383
import { isFallbackTitle } from "@opencode/util/session-title-fallback"
8484
import * as Model from "./util/model"
8585
import { ArgsProvider, useArgs, type Args } from "./context/args"
86-
import open from "open"
86+
import { openUrl } from "@opencode/util/open"
8787
import { PromptRefProvider, usePromptRef } from "./context/prompt"
8888
import { Config, ConfigProvider, useConfig } from "./config"
8989
import { newSessionLocation } from "./config/new-session-location"
@@ -1092,7 +1092,7 @@ function App() {
10921092
name: "docs.open",
10931093
title: "Open docs",
10941094
run: () => {
1095-
open("https://opencode.ai/docs").catch(() => {})
1095+
openUrl("https://opencode.ai/docs").catch(() => {})
10961096
dialog.clear()
10971097
},
10981098
category: "System",

‎packages/tui/src/component/dialog-integration.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
FormValue,
1212
LocationRef,
1313
} from "@opencode/client"
14-
import open from "open"
14+
import { openUrl } from "@opencode/util/open"
1515
import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from "solid-js"
1616
import { useClipboard } from "../context/clipboard"
1717
import { useData } from "../context/data"
@@ -590,7 +590,7 @@ function OAuthAuto(props: {
590590
title: "Open authorization URL",
591591
group: "Dialog",
592592
run: () => {
593-
open(props.attempt.url).catch(() =>
593+
openUrl(props.attempt.url).catch(() =>
594594
toast.show({
595595
message: "Could not open the browser. Copy the URL and continue manually.",
596596
variant: "error",
@@ -985,7 +985,7 @@ async function externalAnswer(
985985
() => <OAuthView title={formLabel(field) || title} message="Opening link…" />,
986986
() => resolve(CANCELLED),
987987
)
988-
void open(field.url).then(
988+
void openUrl(field.url).then(
989989
() => resolve(true),
990990
() => resolve(false),
991991
)

‎packages/tui/src/mini/footer.form.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,8 @@ export function RunFormBody(props: {
228228
try {
229229
if (props.openExternal) await props.openExternal(field.url)
230230
else {
231-
const { default: open } = await import("open")
232-
await open(field.url)
231+
const { openUrl } = await import("@opencode/util/open")
232+
await openUrl(field.url)
233233
}
234234
setState((previous) => formSetExternalReady(previous, field.key))
235235
} catch {

0 commit comments

Comments
 (0)