From b027926cf174252e948e55745b15da2b136d7909 Mon Sep 17 00:00:00 2001 From: "Yule@users.noreply.seal.dev" Date: Thu, 16 Apr 2026 17:22:41 +0000 Subject: [PATCH 01/11] feat: extend attach to auto-auth via CF Access wellknown flow --- packages/opencode/src/auth/auth.ts | 6 ++ packages/opencode/src/cli/cmd/providers.ts | 17 +++- packages/opencode/src/cli/cmd/tui/attach.ts | 74 ++++++++++++++-- packages/opencode/src/cli/cmd/tui/seal.ts | 97 +++++++++++++++++++++ 4 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/seal.ts diff --git a/packages/opencode/src/auth/auth.ts b/packages/opencode/src/auth/auth.ts index fb9d2b149575..375789ca18a9 100644 --- a/packages/opencode/src/auth/auth.ts +++ b/packages/opencode/src/auth/auth.ts @@ -29,6 +29,12 @@ export class WellKnown extends Schema.Class("WellKnownAuth")({ type: Schema.Literal("wellknown"), key: Schema.String, token: Schema.String, + // Command to re-run when the token has expired (stdout becomes the new token). + // Present when the well-known response included an `auth.command` field. + command: Schema.optional(Schema.Array(Schema.String)), + // Unix-second expiry decoded from the token's JWT `exp` claim. + // When set and in the past, `command` is re-run before the token is used. + expires: Schema.optional(Schema.Number), }) {} const _Info = Schema.Union([Oauth, Api, WellKnown]).annotate({ discriminator: "type", identifier: "Auth" }) diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 4bc3f0ea6c3a..652b40b5e07e 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -313,10 +313,25 @@ export const ProvidersLoginCommand = cmd({ prompts.outro("Done") return } + const trimmed = token.trim() + // Decode exp from the JWT payload without verifying the signature so + // the refresh logic in `attach` knows when to re-run the command. + const exp = (() => { + const parts = trimmed.split(".") + if (parts.length !== 3) return undefined + try { + const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString()) + return typeof payload.exp === "number" ? payload.exp : undefined + } catch { + return undefined + } + })() await put(url, { type: "wellknown", key: wellknown.auth.env, - token: token.trim(), + token: trimmed, + command: wellknown.auth.command, + expires: exp, }) prompts.log.success("Logged into " + url) prompts.outro("Done") diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index 9a93f3f57a63..55e12d6f280c 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -3,6 +3,66 @@ import { UI } from "@/cli/ui" import { tui } from "./app" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { TuiConfig } from "@/cli/cmd/tui/config/tui" +import * as prompts from "@clack/prompts" +import { cfAccessToken, listWorkspaces, sandboxSecret } from "./seal" + +// Resolve the TUI target URL and Basic Auth headers. +// +// Three cases: +// 1. --password given → use as-is (existing behaviour, direct attach) +// 2. No --password, URL looks like a Seal base URL (has CF Access credentials +// stored) → auto-fetch the workspace secret via the Seal API +// 3. No --password, no stored credentials → attach without auth (local servers) +async function resolveTarget( + url: string, + password: string | undefined, +): Promise<{ url: string; headers: Record | undefined }> { + const explicit = password ?? process.env.OPENCODE_SERVER_PASSWORD + if (explicit) { + return { + url, + headers: { Authorization: `Basic ${Buffer.from(`opencode:${explicit}`).toString("base64")}` }, + } + } + + // The URL passed to `attach` doubles as the Seal base URL (e.g. + // https://superseal.cloudflare.dev/code). Normalize it the same way + // `opencode auth login` does so the auth.json lookup hits the right key. + const sealBase = url.replace(/\/+$/, "") + + const token = await cfAccessToken(sealBase).catch(() => null) + if (!token) { + return { url, headers: undefined } + } + + // Fetch workspace list from the Seal API. + const workspaces = await listWorkspaces(sealBase).catch((err: unknown) => { + throw new Error(`Failed to fetch workspaces from ${sealBase}: ${err instanceof Error ? err.message : String(err)}`) + }) + + const active = workspaces.filter((w) => w.activeSandbox?.opencodeUrl) + if (active.length === 0) { + throw new Error("No workspaces with an active sandbox found. Start a sandbox from the web UI first.") + } + + const workspace = await (async () => { + if (active.length === 1) return active[0] + const choice = await prompts.select({ + message: "Select workspace", + options: active.map((w) => ({ label: w.name, value: w.id })), + }) + if (prompts.isCancel(choice)) throw new UI.CancelledError() + return active.find((w) => w.id === choice)! + })() + + const secret = await sandboxSecret(sealBase, workspace.id) + const opencodeUrl = new URL(workspace.activeSandbox!.opencodeUrl!).origin + + return { + url: opencodeUrl, + headers: { Authorization: `Basic ${Buffer.from(`opencode:${secret}`).toString("base64")}` }, + } +} export const AttachCommand = cmd({ command: "attach ", @@ -11,7 +71,7 @@ export const AttachCommand = cmd({ yargs .positional("url", { type: "string", - describe: "http://localhost:4096", + describe: "http://localhost:4096 or https://superseal.cloudflare.dev/code", demandOption: true, }) .option("dir", { @@ -58,15 +118,11 @@ export const AttachCommand = cmd({ return args.dir } })() - const headers = (() => { - const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD - if (!password) return undefined - const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` - return { Authorization: auth } - })() + + const target = await resolveTarget(args.url, args.password) const config = await TuiConfig.get() await tui({ - url: args.url, + url: target.url, config, args: { continue: args.continue, @@ -74,7 +130,7 @@ export const AttachCommand = cmd({ fork: args.fork, }, directory, - headers, + headers: target.headers, }) } finally { unguard?.() diff --git a/packages/opencode/src/cli/cmd/tui/seal.ts b/packages/opencode/src/cli/cmd/tui/seal.ts new file mode 100644 index 000000000000..040a465aa1c1 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/seal.ts @@ -0,0 +1,97 @@ +import { text } from "node:stream/consumers" +import { AppRuntime } from "@/effect/app-runtime" +import { Auth } from "@/auth" +import { Process } from "@/util" +import { Effect } from "effect" + +// Decode the `exp` claim from a JWT without verifying the signature. +// Returns undefined if the token is not a valid JWT or has no exp. +function jwtExp(token: string): number | undefined { + const parts = token.split(".") + if (parts.length !== 3) return undefined + try { + const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString()) + return typeof payload.exp === "number" ? payload.exp : undefined + } catch { + return undefined + } +} + +function isExpired(expires: number | undefined): boolean { + if (expires === undefined) return false + // Refresh 60 seconds before actual expiry so we don't send a just-expired token. + return Date.now() / 1000 > expires - 60 +} + +// Run the stored command and return its stdout trimmed. +async function runCommand(command: readonly string[]): Promise { + const proc = Process.spawn([...command], { stdout: "pipe" }) + if (!proc.stdout) throw new Error(`command produced no stdout: ${command[0]}`) + const [exit, output] = await Promise.all([proc.exited, text(proc.stdout)]) + if (exit !== 0) throw new Error(`command exited ${exit}: ${command[0]}`) + return output.trim() +} + +// Return the CF Access token for `baseUrl`, refreshing via the stored command +// if the cached token is expired. Throws if no wellknown entry exists. +export async function cfAccessToken(baseUrl: string): Promise { + const norm = baseUrl.replace(/\/+$/, "") + const auth = await AppRuntime.runPromise( + Effect.gen(function* () { + const svc = yield* Auth.Service + return yield* svc.get(norm) + }), + ) + + if (!auth || auth.type !== "wellknown") { + throw new Error(`No CF Access credentials stored for ${norm}. Run: opencode auth login ${norm}`) + } + + if (!isExpired(auth.expires)) return auth.token + + if (!auth.command || auth.command.length === 0) { + throw new Error(`Token for ${norm} is expired and no refresh command is stored. Run: opencode auth login ${norm}`) + } + + const token = await runCommand(auth.command) + const expires = jwtExp(token) + + await AppRuntime.runPromise( + Effect.gen(function* () { + const svc = yield* Auth.Service + yield* svc.set(norm, { ...auth, token, expires }) + }), + ) + + return token +} + +type SealWorkspace = { + id: string + name: string + activeSandbox: { opencodeUrl: string } | null +} + +// Fetch the list of workspaces from the Seal API using the stored CF Access token. +export async function listWorkspaces(baseUrl: string): Promise { + const norm = baseUrl.replace(/\/+$/, "") + const token = await cfAccessToken(norm) + const res = await fetch(`${norm}/api/workspaces`, { + headers: { "cf-access-jwt-assertion": token }, + }) + if (!res.ok) throw new Error(`Seal API returned ${res.status} fetching workspaces`) + const data = (await res.json()) as { workspaces: SealWorkspace[] } + return data.workspaces +} + +// Fetch the per-sandbox Basic Auth secret for `workspaceId`. +export async function sandboxSecret(baseUrl: string, workspaceId: string): Promise { + const norm = baseUrl.replace(/\/+$/, "") + const token = await cfAccessToken(norm) + const res = await fetch(`${norm}/api/workspaces/${workspaceId}/sandbox-secret`, { + headers: { "cf-access-jwt-assertion": token }, + }) + if (!res.ok) throw new Error(`Seal API returned ${res.status} fetching sandbox secret`) + const data = (await res.json()) as { secret: string } + return data.secret +} From 17f165b69073cf1da2e6cda46acccddf4b1126a9 Mon Sep 17 00:00:00 2001 From: "Yule@users.noreply.seal.dev" Date: Thu, 16 Apr 2026 17:31:55 +0000 Subject: [PATCH 02/11] fix: correct workspace list and sandbox connect flow in attach command --- packages/opencode/src/cli/cmd/tui/attach.ts | 34 ++++++++++++--------- packages/opencode/src/cli/cmd/tui/seal.ts | 19 +++++++++--- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index 55e12d6f280c..1717e9bc3202 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -4,15 +4,15 @@ import { tui } from "./app" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { TuiConfig } from "@/cli/cmd/tui/config/tui" import * as prompts from "@clack/prompts" -import { cfAccessToken, listWorkspaces, sandboxSecret } from "./seal" +import { cfAccessToken, listWorkspaces, sandboxConnect } from "./seal" // Resolve the TUI target URL and Basic Auth headers. // // Three cases: -// 1. --password given → use as-is (existing behaviour, direct attach) -// 2. No --password, URL looks like a Seal base URL (has CF Access credentials -// stored) → auto-fetch the workspace secret via the Seal API -// 3. No --password, no stored credentials → attach without auth (local servers) +// 1. --password given (or OPENCODE_SERVER_PASSWORD set) → use as-is (direct attach) +// 2. No password, CF Access credentials stored for the URL → auto-fetch workspace +// secret + preview URL from the Seal API +// 3. No password, no stored credentials → attach without auth (local server) async function resolveTarget( url: string, password: string | undefined, @@ -32,6 +32,7 @@ async function resolveTarget( const token = await cfAccessToken(sealBase).catch(() => null) if (!token) { + // No CF Access credentials stored — treat as a plain local server. return { url, headers: undefined } } @@ -40,27 +41,32 @@ async function resolveTarget( throw new Error(`Failed to fetch workspaces from ${sealBase}: ${err instanceof Error ? err.message : String(err)}`) }) - const active = workspaces.filter((w) => w.activeSandbox?.opencodeUrl) - if (active.length === 0) { - throw new Error("No workspaces with an active sandbox found. Start a sandbox from the web UI first.") + if (workspaces.length === 0) { + throw new Error("No workspaces found. Create one from the web UI first.") } const workspace = await (async () => { - if (active.length === 1) return active[0] + if (workspaces.length === 1) return workspaces[0] const choice = await prompts.select({ message: "Select workspace", - options: active.map((w) => ({ label: w.name, value: w.id })), + options: workspaces.map((w) => ({ label: w.name, value: w.id })), }) if (prompts.isCancel(choice)) throw new UI.CancelledError() - return active.find((w) => w.id === choice)! + return workspaces.find((w) => w.id === choice)! })() - const secret = await sandboxSecret(sealBase, workspace.id) - const opencodeUrl = new URL(workspace.activeSandbox!.opencodeUrl!).origin + const connect = await sandboxConnect(sealBase, workspace.id) + if (!connect) { + throw new Error(`No active sandbox for workspace "${workspace.name}". Start one from the web UI first.`) + } + if (!connect.opencodeUrl) { + throw new Error(`Sandbox for workspace "${workspace.name}" is still starting. Try again in a moment.`) + } + const opencodeUrl = new URL(connect.opencodeUrl).origin return { url: opencodeUrl, - headers: { Authorization: `Basic ${Buffer.from(`opencode:${secret}`).toString("base64")}` }, + headers: { Authorization: `Basic ${Buffer.from(`opencode:${connect.secret}`).toString("base64")}` }, } } diff --git a/packages/opencode/src/cli/cmd/tui/seal.ts b/packages/opencode/src/cli/cmd/tui/seal.ts index 040a465aa1c1..f8b28212b340 100644 --- a/packages/opencode/src/cli/cmd/tui/seal.ts +++ b/packages/opencode/src/cli/cmd/tui/seal.ts @@ -69,10 +69,11 @@ export async function cfAccessToken(baseUrl: string): Promise { type SealWorkspace = { id: string name: string - activeSandbox: { opencodeUrl: string } | null } // Fetch the list of workspaces from the Seal API using the stored CF Access token. +// WorkspaceInfo (returned by GET /workspaces) contains only id/name/status — +// sandbox state lives on WorkspaceDO, not in the list response. export async function listWorkspaces(baseUrl: string): Promise { const norm = baseUrl.replace(/\/+$/, "") const token = await cfAccessToken(norm) @@ -84,14 +85,22 @@ export async function listWorkspaces(baseUrl: string): Promise return data.workspaces } -// Fetch the per-sandbox Basic Auth secret for `workspaceId`. -export async function sandboxSecret(baseUrl: string, workspaceId: string): Promise { +type SandboxConnect = { + secret: string + // The preview URL for the sandbox's OpenCode port. Null when the sandbox + // is not yet active (container still starting or not provisioned). + opencodeUrl: string | null +} + +// Fetch the per-sandbox Basic Auth secret and preview URL for `workspaceId`. +// Returns null when no active sandbox exists for the workspace (404). +export async function sandboxConnect(baseUrl: string, workspaceId: string): Promise { const norm = baseUrl.replace(/\/+$/, "") const token = await cfAccessToken(norm) const res = await fetch(`${norm}/api/workspaces/${workspaceId}/sandbox-secret`, { headers: { "cf-access-jwt-assertion": token }, }) + if (res.status === 404) return null if (!res.ok) throw new Error(`Seal API returned ${res.status} fetching sandbox secret`) - const data = (await res.json()) as { secret: string } - return data.secret + return res.json() as Promise } From 32d4aefb9124ac289f50af5a5529b557d573158c Mon Sep 17 00:00:00 2001 From: "Yule@users.noreply.seal.dev" Date: Thu, 16 Apr 2026 18:16:35 +0000 Subject: [PATCH 03/11] fix: error handling in wellknown login and retry loop for sandbox start in attach --- packages/opencode/src/cli/cmd/providers.ts | 20 +++++++++++++- packages/opencode/src/cli/cmd/tui/attach.ts | 29 +++++++++++++++------ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 652b40b5e07e..ebe46715289f 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -297,7 +297,25 @@ export const ProvidersLoginCommand = cmd({ prompts.intro("Add credential") if (args.url) { const url = args.url.replace(/\/+$/, "") - const wellknown = await fetch(`${url}/.well-known/opencode`).then((x) => x.json() as any) + let wellknown: any + try { + const res = await fetch(`${url}/.well-known/opencode`) + if (!res.ok) { + prompts.log.error(`Server returned ${res.status} for ${url}/.well-known/opencode`) + prompts.outro("Done") + return + } + wellknown = await res.json() + } catch { + prompts.log.error(`Could not reach ${url}`) + prompts.outro("Done") + return + } + if (!Array.isArray(wellknown?.auth?.command) || wellknown.auth.command.length === 0) { + prompts.log.error("Server did not return a valid auth.command") + prompts.outro("Done") + return + } prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``) const proc = Process.spawn(wellknown.auth.command, { stdout: "pipe", diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index 1717e9bc3202..b1cb13718e1a 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -55,15 +55,28 @@ async function resolveTarget( return workspaces.find((w) => w.id === choice)! })() - const connect = await sandboxConnect(sealBase, workspace.id) - if (!connect) { - throw new Error(`No active sandbox for workspace "${workspace.name}". Start one from the web UI first.`) - } - if (!connect.opencodeUrl) { - throw new Error(`Sandbox for workspace "${workspace.name}" is still starting. Try again in a moment.`) - } + // Poll until the sandbox's opencode port is exposed (container may still be + // starting). Retry every 5 seconds for up to 60 seconds before giving up. + const spinner = prompts.spinner() + spinner.start(`Waiting for sandbox to start…`) + const connect = await (async () => { + const MAX_ATTEMPTS = 12 + const DELAY_MS = 5000 + for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { + const result = await sandboxConnect(sealBase, workspace.id) + if (!result) { + spinner.stop("No active sandbox") + throw new Error(`No active sandbox for workspace "${workspace.name}". Start one from the web UI first.`) + } + if (result.opencodeUrl) return result + if (attempt < MAX_ATTEMPTS - 1) await new Promise((r) => setTimeout(r, DELAY_MS)) + } + spinner.stop("Timed out") + throw new Error(`Sandbox for workspace "${workspace.name}" did not become ready within 60 seconds.`) + })() + spinner.stop("Sandbox ready") - const opencodeUrl = new URL(connect.opencodeUrl).origin + const opencodeUrl = new URL(connect.opencodeUrl!).origin return { url: opencodeUrl, headers: { Authorization: `Basic ${Buffer.from(`opencode:${connect.secret}`).toString("base64")}` }, From 832b9df51917f59cadd7013e50536b9a79ec58ed Mon Sep 17 00:00:00 2001 From: "byule@cloudflare.com" Date: Fri, 1 May 2026 10:26:35 -0400 Subject: [PATCH 04/11] feat: add --header/-H flag to attach and run --attach for custom auth headers --- packages/opencode/src/cli/cmd/run.ts | 25 +++++++++++++++-- packages/opencode/src/cli/cmd/tui/attach.ts | 30 ++++++++++++++++++--- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 0874beee16c8..22089bf75e10 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -62,6 +62,20 @@ function block(info: Inline, output?: string) { UI.empty() } +function parseHeaders(values: string[] | undefined): Record | undefined { + if (!values || values.length === 0) return undefined + const result: Record = {} + for (const value of values) { + const idx = value.indexOf(":") + if (idx === -1) throw new Error(`Invalid header format: "${value}". Expected "Key: Value"`) + const key = value.slice(0, idx).trim() + const val = value.slice(idx + 1).trim() + if (!key) throw new Error(`Invalid header format: "${value}". Missing key.`) + result[key] = val + } + return result +} + function fallback(part: ToolPart) { const state = part.state const input = "input" in state ? state.input : undefined @@ -277,6 +291,12 @@ export const RunCommand = cmd({ type: "string", describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)", }) + .option("header", { + alias: ["H"], + type: "string", + array: true, + describe: "custom header to send in the format 'Key: Value' (can be specified multiple times)", + }) .option("dir", { type: "string", describe: "directory to run in, path on remote server if attaching", @@ -660,10 +680,11 @@ export const RunCommand = cmd({ if (args.attach) { const headers = (() => { const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD - if (!password) return undefined + const custom = parseHeaders(args.header) + if (!password) return custom const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode" const auth = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` - return { Authorization: auth } + return { Authorization: auth, ...custom } })() const sdk = createOpencodeClient({ baseUrl: args.attach, directory, headers }) return await execute(sdk) diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index b1cb13718e1a..250529799481 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -6,6 +6,20 @@ import { TuiConfig } from "@/cli/cmd/tui/config/tui" import * as prompts from "@clack/prompts" import { cfAccessToken, listWorkspaces, sandboxConnect } from "./seal" +function parseHeaders(values: string[] | undefined): Record | undefined { + if (!values || values.length === 0) return undefined + const result: Record = {} + for (const value of values) { + const idx = value.indexOf(":") + if (idx === -1) throw new Error(`Invalid header format: "${value}". Expected "Key: Value"`) + const key = value.slice(0, idx).trim() + const val = value.slice(idx + 1).trim() + if (!key) throw new Error(`Invalid header format: "${value}". Missing key.`) + result[key] = val + } + return result +} + // Resolve the TUI target URL and Basic Auth headers. // // Three cases: @@ -16,12 +30,13 @@ import { cfAccessToken, listWorkspaces, sandboxConnect } from "./seal" async function resolveTarget( url: string, password: string | undefined, + customHeaders: Record | undefined, ): Promise<{ url: string; headers: Record | undefined }> { const explicit = password ?? process.env.OPENCODE_SERVER_PASSWORD if (explicit) { return { url, - headers: { Authorization: `Basic ${Buffer.from(`opencode:${explicit}`).toString("base64")}` }, + headers: { Authorization: `Basic ${Buffer.from(`opencode:${explicit}`).toString("base64")}`, ...customHeaders }, } } @@ -33,7 +48,7 @@ async function resolveTarget( const token = await cfAccessToken(sealBase).catch(() => null) if (!token) { // No CF Access credentials stored — treat as a plain local server. - return { url, headers: undefined } + return { url, headers: customHeaders } } // Fetch workspace list from the Seal API. @@ -79,7 +94,7 @@ async function resolveTarget( const opencodeUrl = new URL(connect.opencodeUrl!).origin return { url: opencodeUrl, - headers: { Authorization: `Basic ${Buffer.from(`opencode:${connect.secret}`).toString("base64")}` }, + headers: { Authorization: `Basic ${Buffer.from(`opencode:${connect.secret}`).toString("base64")}`, ...customHeaders }, } } @@ -115,6 +130,12 @@ export const AttachCommand = cmd({ alias: ["p"], type: "string", describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)", + }) + .option("header", { + alias: ["H"], + type: "string", + array: true, + describe: "custom header to send in the format 'Key: Value' (can be specified multiple times)", }), handler: async (args) => { const unguard = win32InstallCtrlCGuard() @@ -138,7 +159,8 @@ export const AttachCommand = cmd({ } })() - const target = await resolveTarget(args.url, args.password) + const customHeaders = parseHeaders(args.header) + const target = await resolveTarget(args.url, args.password, customHeaders) const config = await TuiConfig.get() await tui({ url: target.url, From 3bc485d7106ec2cc702407cdc135ef040594b2c2 Mon Sep 17 00:00:00 2001 From: "byule@cloudflare.com" Date: Fri, 1 May 2026 14:38:47 -0400 Subject: [PATCH 05/11] feat: add install-dev.sh for easy dev branch setup --- install-dev.sh | 159 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100755 install-dev.sh diff --git a/install-dev.sh b/install-dev.sh new file mode 100755 index 000000000000..610ac13504ed --- /dev/null +++ b/install-dev.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP=opencode +REPO="https://github.com/byule/opencode.git" +BRANCH="dev" +INSTALL_DIR="$HOME/.opencode-dev" + +MUTED='\033[0;2m' +RED='\033[0;31m' +ORANGE='\033[38;5;214m' +GREEN='\033[0;32m' +NC='\033[0m' + +print_message() { + local level=$1 + local message=$2 + local color="" + case $level in + info) color="${NC}" ;; + success) color="${GREEN}" ;; + warning) color="${ORANGE}" ;; + error) color="${RED}" ;; + esac + echo -e "${color}${message}${NC}" +} + +# Check prerequisites +print_message info "Checking prerequisites..." + +if ! command -v git >/dev/null 2>&1; then + print_message error "git is required but not installed. Install it first:" + echo " macOS: brew install git" + echo " Ubuntu/Debian: sudo apt-get install git" + exit 1 +fi + +if ! command -v bun >/dev/null 2>&1; then + print_message error "bun is required but not installed. Install it first:" + echo " curl -fsSL https://bun.sh/install | bash" + exit 1 +fi + +print_message success " git: $(git --version | awk '{print $3}')" +print_message success " bun: $(bun --version)" + +# Clone or update the repository +if [ -d "$INSTALL_DIR/.git" ]; then + print_message info "\n${MUTED}Updating existing repo at ${NC}$INSTALL_DIR" + cd "$INSTALL_DIR" + git fetch origin + git checkout "$BRANCH" + git reset --hard "origin/$BRANCH" +else + print_message info "\n${MUTED}Cloning ${NC}byule/opencode${MUTED} (${BRANCH} branch) into ${NC}$INSTALL_DIR" + rm -rf "$INSTALL_DIR" + git clone --branch "$BRANCH" --single-branch "$REPO" "$INSTALL_DIR" +fi + +# Install dependencies +print_message info "\n${MUTED}Installing dependencies...${NC}" +cd "$INSTALL_DIR" +bun install + +# Set up shell alias +print_message info "\n${MUTED}Configuring shell alias...${NC}" + +ALIAS_CMD="alias opencode='bun run --cwd $INSTALL_DIR/packages/opencode dev --'" + +current_shell=$(basename "$SHELL") +case $current_shell in + zsh) + config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv" + ;; + bash) + config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile" + ;; + fish) + config_files="$HOME/.config/fish/config.fish" + ALIAS_CMD="alias opencode 'bun run --cwd $INSTALL_DIR/packages/opencode dev --'" + ;; + *) + config_files="$HOME/.bashrc $HOME/.profile" + ;; +esac + +config_file="" +for file in $config_files; do + if [ -f "$file" ]; then + config_file=$file + break + fi +done + +if [ -z "$config_file" ]; then + config_file="$HOME/.zshrc" + [ "$current_shell" = "bash" ] && config_file="$HOME/.bashrc" +fi + +add_alias() { + local file=$1 + local cmd=$2 + + if grep -Fq "$INSTALL_DIR/packages/opencode" "$file" 2>/dev/null; then + print_message warning " opencode alias already exists in $(basename "$file"), skipping." + else + echo "" >> "$file" + echo "# opencode dev branch (byule fork)" >> "$file" + echo "$cmd" >> "$file" + print_message success " Added alias to $(basename "$file")" + fi +} + +add_alias "$config_file" "$ALIAS_CMD" + +# Also add a convenience wrapper for CF Access +cat > "$INSTALL_DIR/attach-cf" <<'WRAPPER' +#!/usr/bin/env bash +set -euo pipefail + +URL="${1:-}" +if [ -z "$URL" ]; then + echo "Usage: attach-cf " + echo "Example: attach-cf https://4096-sb-867770819f83-j6kaxtmu0u7opzod.superseal.cloudflare.dev/" + exit 1 +fi + +if ! command -v cloudflared >/dev/null 2>&1; then + echo "cloudflared is required. Install it: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" + exit 1 +fi + +TOKEN=$(cloudflared access token --app="$URL") +exec opencode attach "$URL" -H "cf-access-token: $TOKEN" +WRAPPER +chmod +x "$INSTALL_DIR/attach-cf" + +# Summary +echo "" +echo -e "${MUTED}  ${NC} ▄ " +echo -e "${MUTED}█▀▀█ █▀▀█ █▀▀█ █▀▀▄ ${NC}█▀▀▀ █▀▀█ █▀▀█ █▀▀█" +echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░░░ █░░█ █░░█ █▀▀▀" +echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀" +echo -e "" +print_message success "OpenCode (dev branch) installed successfully!" +echo "" +echo -e "${MUTED}To use it now, run:${NC}" +echo -e " source $(basename "$config_file")" +echo "" +echo -e "${MUTED}Then you can run:${NC}" +echo -e " opencode --version ${MUTED}# Should print 'local'${NC}" +echo -e " opencode attach -H \"cf-access-token: \"" +echo "" +echo -e "${MUTED}For CF Access-protected servers:${NC}" +echo -e " $INSTALL_DIR/attach-cf ${MUTED}# Auto-fetches token via cloudflared${NC}" +echo "" +echo -e "${MUTED}Example:${NC}" +echo -e " attach-cf https://4096-sb-867770819f83-j6kaxtmu0u7opzod.superseal.cloudflare.dev/" +echo "" From 09f971da050bf1834aeb321310c8591f98e0ff9b Mon Sep 17 00:00:00 2001 From: "byule@cloudflare.com" Date: Fri, 1 May 2026 14:42:35 -0400 Subject: [PATCH 06/11] fix: use wrapper scripts instead of shell alias for proper PATH integration --- install-dev.sh | 106 ++++++++++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/install-dev.sh b/install-dev.sh index 610ac13504ed..210b59afb885 100755 --- a/install-dev.sh +++ b/install-dev.sh @@ -5,6 +5,7 @@ APP=opencode REPO="https://github.com/byule/opencode.git" BRANCH="dev" INSTALL_DIR="$HOME/.opencode-dev" +BIN_DIR="$INSTALL_DIR/bin" MUTED='\033[0;2m' RED='\033[0;31m' @@ -62,59 +63,17 @@ print_message info "\n${MUTED}Installing dependencies...${NC}" cd "$INSTALL_DIR" bun install -# Set up shell alias -print_message info "\n${MUTED}Configuring shell alias...${NC}" - -ALIAS_CMD="alias opencode='bun run --cwd $INSTALL_DIR/packages/opencode dev --'" - -current_shell=$(basename "$SHELL") -case $current_shell in - zsh) - config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv" - ;; - bash) - config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile" - ;; - fish) - config_files="$HOME/.config/fish/config.fish" - ALIAS_CMD="alias opencode 'bun run --cwd $INSTALL_DIR/packages/opencode dev --'" - ;; - *) - config_files="$HOME/.bashrc $HOME/.profile" - ;; -esac - -config_file="" -for file in $config_files; do - if [ -f "$file" ]; then - config_file=$file - break - fi -done - -if [ -z "$config_file" ]; then - config_file="$HOME/.zshrc" - [ "$current_shell" = "bash" ] && config_file="$HOME/.bashrc" -fi +# Create wrapper scripts +print_message info "\n${MUTED}Creating wrapper scripts in ${NC}$BIN_DIR" +mkdir -p "$BIN_DIR" -add_alias() { - local file=$1 - local cmd=$2 - - if grep -Fq "$INSTALL_DIR/packages/opencode" "$file" 2>/dev/null; then - print_message warning " opencode alias already exists in $(basename "$file"), skipping." - else - echo "" >> "$file" - echo "# opencode dev branch (byule fork)" >> "$file" - echo "$cmd" >> "$file" - print_message success " Added alias to $(basename "$file")" - fi -} - -add_alias "$config_file" "$ALIAS_CMD" +cat > "$BIN_DIR/opencode" <<'WRAPPER' +#!/usr/bin/env bash +exec bun run --cwd "$HOME/.opencode-dev/packages/opencode" dev -- "$@" +WRAPPER +chmod +x "$BIN_DIR/opencode" -# Also add a convenience wrapper for CF Access -cat > "$INSTALL_DIR/attach-cf" <<'WRAPPER' +cat > "$BIN_DIR/attach-cf" <<'WRAPPER' #!/usr/bin/env bash set -euo pipefail @@ -133,7 +92,44 @@ fi TOKEN=$(cloudflared access token --app="$URL") exec opencode attach "$URL" -H "cf-access-token: $TOKEN" WRAPPER -chmod +x "$INSTALL_DIR/attach-cf" +chmod +x "$BIN_DIR/attach-cf" + +# Add to PATH if not already there +print_message info "\n${MUTED}Configuring PATH...${NC}" + +if [[ ":$PATH:" == *":$BIN_DIR:"* ]]; then + print_message success " $BIN_DIR is already in your PATH" +else + current_shell=$(basename "$SHELL") + case $current_shell in + zsh) + config_file="${ZDOTDIR:-$HOME}/.zshrc" + ;; + bash) + config_file="$HOME/.bashrc" + ;; + fish) + config_file="$HOME/.config/fish/config.fish" + ;; + *) + config_file="$HOME/.profile" + ;; + esac + + if [ -f "$config_file" ]; then + if grep -Fq "$BIN_DIR" "$config_file" 2>/dev/null; then + print_message warning " PATH entry already exists in $(basename "$config_file"), skipping." + else + echo "" >> "$config_file" + echo "# opencode dev branch (byule fork)" >> "$config_file" + echo "export PATH=\"$BIN_DIR:\$PATH\"" >> "$config_file" + print_message success " Added $BIN_DIR to PATH in $(basename "$config_file")" + fi + else + print_message warning " Could not find shell config. Add this manually:" + echo " export PATH=\"$BIN_DIR:\$PATH\"" + fi +fi # Summary echo "" @@ -145,14 +141,14 @@ echo -e "" print_message success "OpenCode (dev branch) installed successfully!" echo "" echo -e "${MUTED}To use it now, run:${NC}" -echo -e " source $(basename "$config_file")" +echo -e " source $(basename "$config_file" 2>/dev/null || echo 'your shell config')" echo "" -echo -e "${MUTED}Then you can run:${NC}" +echo -e "${MUTED}Then verify:${NC}" echo -e " opencode --version ${MUTED}# Should print 'local'${NC}" echo -e " opencode attach -H \"cf-access-token: \"" echo "" echo -e "${MUTED}For CF Access-protected servers:${NC}" -echo -e " $INSTALL_DIR/attach-cf ${MUTED}# Auto-fetches token via cloudflared${NC}" +echo -e " attach-cf ${MUTED}# Auto-fetches token via cloudflared${NC}" echo "" echo -e "${MUTED}Example:${NC}" echo -e " attach-cf https://4096-sb-867770819f83-j6kaxtmu0u7opzod.superseal.cloudflare.dev/" From 74a6db6e0b82a7ed424bef21632139bea40bb40e Mon Sep 17 00:00:00 2001 From: "byule@cloudflare.com" Date: Fri, 1 May 2026 14:47:04 -0400 Subject: [PATCH 07/11] feat: rename to cfcode with URL shortcut for attach --- install-dev.sh | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/install-dev.sh b/install-dev.sh index 210b59afb885..e279184e83cb 100755 --- a/install-dev.sh +++ b/install-dev.sh @@ -1,10 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -APP=opencode REPO="https://github.com/byule/opencode.git" BRANCH="dev" -INSTALL_DIR="$HOME/.opencode-dev" +INSTALL_DIR="$HOME/.cfcode" BIN_DIR="$INSTALL_DIR/bin" MUTED='\033[0;2m' @@ -67,11 +66,23 @@ bun install print_message info "\n${MUTED}Creating wrapper scripts in ${NC}$BIN_DIR" mkdir -p "$BIN_DIR" -cat > "$BIN_DIR/opencode" <<'WRAPPER' +cat > "$BIN_DIR/cfcode" <<'WRAPPER' #!/usr/bin/env bash -exec bun run --cwd "$HOME/.opencode-dev/packages/opencode" dev -- "$@" +set -euo pipefail + +INSTALL_DIR="$HOME/.cfcode" + +# If first argument looks like a URL, treat it as 'attach ' +if [ $# -gt 0 ] && [[ "${1:-}" =~ ^https?:// ]]; then + URL="$1" + shift + exec bun run --cwd "$INSTALL_DIR/packages/opencode" dev -- attach "$URL" "$@" +fi + +# Otherwise pass through to opencode normally +exec bun run --cwd "$INSTALL_DIR/packages/opencode" dev -- "$@" WRAPPER -chmod +x "$BIN_DIR/opencode" +chmod +x "$BIN_DIR/cfcode" cat > "$BIN_DIR/attach-cf" <<'WRAPPER' #!/usr/bin/env bash @@ -90,7 +101,7 @@ if ! command -v cloudflared >/dev/null 2>&1; then fi TOKEN=$(cloudflared access token --app="$URL") -exec opencode attach "$URL" -H "cf-access-token: $TOKEN" +exec cfcode attach "$URL" -H "cf-access-token: $TOKEN" WRAPPER chmod +x "$BIN_DIR/attach-cf" @@ -121,7 +132,7 @@ else print_message warning " PATH entry already exists in $(basename "$config_file"), skipping." else echo "" >> "$config_file" - echo "# opencode dev branch (byule fork)" >> "$config_file" + echo "# cfcode (byule's opencode fork)" >> "$config_file" echo "export PATH=\"$BIN_DIR:\$PATH\"" >> "$config_file" print_message success " Added $BIN_DIR to PATH in $(basename "$config_file")" fi @@ -138,18 +149,22 @@ echo -e "${MUTED}█▀▀█ █▀▀█ █▀▀█ █▀▀▄ ${NC}█▀ echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░░░ █░░█ █░░█ █▀▀▀" echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀" echo -e "" -print_message success "OpenCode (dev branch) installed successfully!" +print_message success "cfcode installed successfully!" echo "" echo -e "${MUTED}To use it now, run:${NC}" echo -e " source $(basename "$config_file" 2>/dev/null || echo 'your shell config')" echo "" echo -e "${MUTED}Then verify:${NC}" -echo -e " opencode --version ${MUTED}# Should print 'local'${NC}" -echo -e " opencode attach -H \"cf-access-token: \"" +echo -e " cfcode --version ${MUTED}# Should print 'local'${NC}" +echo -e " cfcode attach -H \"cf-access-token: \"" +echo "" +echo -e "${MUTED}Quick connect to a remote URL:${NC}" +echo -e " cfcode ${MUTED}# Shorthand for 'cfcode attach '${NC}" echo "" echo -e "${MUTED}For CF Access-protected servers:${NC}" echo -e " attach-cf ${MUTED}# Auto-fetches token via cloudflared${NC}" echo "" -echo -e "${MUTED}Example:${NC}" +echo -e "${MUTED}Examples:${NC}" +echo -e " cfcode https://4096-sb-867770819f83-j6kaxtmu0u7opzod.superseal.cloudflare.dev/" echo -e " attach-cf https://4096-sb-867770819f83-j6kaxtmu0u7opzod.superseal.cloudflare.dev/" echo "" From 2c4470f2bc93aa2b6c2dfbe29d4b96658d4039bb Mon Sep 17 00:00:00 2001 From: "byule@cloudflare.com" Date: Fri, 1 May 2026 14:54:22 -0400 Subject: [PATCH 08/11] feat: cfcode auto-fetches CF Access token via cloudflared --- install-dev.sh | 42 ++++++++++++++---------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/install-dev.sh b/install-dev.sh index e279184e83cb..1934e9d92033 100755 --- a/install-dev.sh +++ b/install-dev.sh @@ -76,7 +76,18 @@ INSTALL_DIR="$HOME/.cfcode" if [ $# -gt 0 ] && [[ "${1:-}" =~ ^https?:// ]]; then URL="$1" shift - exec bun run --cwd "$INSTALL_DIR/packages/opencode" dev -- attach "$URL" "$@" + + # Try to auto-fetch a Cloudflare Access token + TOKEN="" + if command -v cloudflared >/dev/null 2>&1; then + TOKEN=$(cloudflared access token --app="$URL" 2>/dev/null || true) + fi + + if [ -n "$TOKEN" ]; then + exec bun run --cwd "$INSTALL_DIR/packages/opencode" dev -- attach "$URL" -H "cf-access-token: $TOKEN" "$@" + else + exec bun run --cwd "$INSTALL_DIR/packages/opencode" dev -- attach "$URL" "$@" + fi fi # Otherwise pass through to opencode normally @@ -84,27 +95,6 @@ exec bun run --cwd "$INSTALL_DIR/packages/opencode" dev -- "$@" WRAPPER chmod +x "$BIN_DIR/cfcode" -cat > "$BIN_DIR/attach-cf" <<'WRAPPER' -#!/usr/bin/env bash -set -euo pipefail - -URL="${1:-}" -if [ -z "$URL" ]; then - echo "Usage: attach-cf " - echo "Example: attach-cf https://4096-sb-867770819f83-j6kaxtmu0u7opzod.superseal.cloudflare.dev/" - exit 1 -fi - -if ! command -v cloudflared >/dev/null 2>&1; then - echo "cloudflared is required. Install it: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" - exit 1 -fi - -TOKEN=$(cloudflared access token --app="$URL") -exec cfcode attach "$URL" -H "cf-access-token: $TOKEN" -WRAPPER -chmod +x "$BIN_DIR/attach-cf" - # Add to PATH if not already there print_message info "\n${MUTED}Configuring PATH...${NC}" @@ -159,12 +149,8 @@ echo -e " cfcode --version ${MUTED}# Should print 'local'${NC}" echo -e " cfcode attach -H \"cf-access-token: \"" echo "" echo -e "${MUTED}Quick connect to a remote URL:${NC}" -echo -e " cfcode ${MUTED}# Shorthand for 'cfcode attach '${NC}" -echo "" -echo -e "${MUTED}For CF Access-protected servers:${NC}" -echo -e " attach-cf ${MUTED}# Auto-fetches token via cloudflared${NC}" +echo -e " cfcode ${MUTED}# Attaches and auto-fetches CF Access token if available${NC}" echo "" -echo -e "${MUTED}Examples:${NC}" +echo -e "${MUTED}Example:${NC}" echo -e " cfcode https://4096-sb-867770819f83-j6kaxtmu0u7opzod.superseal.cloudflare.dev/" -echo -e " attach-cf https://4096-sb-867770819f83-j6kaxtmu0u7opzod.superseal.cloudflare.dev/" echo "" From a6b12365af635b466db97067905f9563fb5af5e1 Mon Sep 17 00:00:00 2001 From: "byule@cloudflare.com" Date: Fri, 1 May 2026 15:01:33 -0400 Subject: [PATCH 09/11] fix: config_file unbound variable when PATH already contains cfcode bin --- install-dev.sh | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/install-dev.sh b/install-dev.sh index 1934e9d92033..8d471ac51f4c 100755 --- a/install-dev.sh +++ b/install-dev.sh @@ -95,28 +95,29 @@ exec bun run --cwd "$INSTALL_DIR/packages/opencode" dev -- "$@" WRAPPER chmod +x "$BIN_DIR/cfcode" +# Determine shell config file +current_shell=$(basename "$SHELL") +case $current_shell in + zsh) + config_file="${ZDOTDIR:-$HOME}/.zshrc" + ;; + bash) + config_file="$HOME/.bashrc" + ;; + fish) + config_file="$HOME/.config/fish/config.fish" + ;; + *) + config_file="$HOME/.profile" + ;; +esac + # Add to PATH if not already there print_message info "\n${MUTED}Configuring PATH...${NC}" if [[ ":$PATH:" == *":$BIN_DIR:"* ]]; then print_message success " $BIN_DIR is already in your PATH" else - current_shell=$(basename "$SHELL") - case $current_shell in - zsh) - config_file="${ZDOTDIR:-$HOME}/.zshrc" - ;; - bash) - config_file="$HOME/.bashrc" - ;; - fish) - config_file="$HOME/.config/fish/config.fish" - ;; - *) - config_file="$HOME/.profile" - ;; - esac - if [ -f "$config_file" ]; then if grep -Fq "$BIN_DIR" "$config_file" 2>/dev/null; then print_message warning " PATH entry already exists in $(basename "$config_file"), skipping." From 0a5e57bc2bd279295b7ba95fe2a5451ba37690f7 Mon Sep 17 00:00:00 2001 From: "byule@cloudflare.com" Date: Fri, 1 May 2026 15:59:54 -0400 Subject: [PATCH 10/11] fix: cloudflared is required for cfcode , no silent fallbacks --- install-dev.sh | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/install-dev.sh b/install-dev.sh index 8d471ac51f4c..06e15cebde10 100755 --- a/install-dev.sh +++ b/install-dev.sh @@ -77,17 +77,17 @@ if [ $# -gt 0 ] && [[ "${1:-}" =~ ^https?:// ]]; then URL="$1" shift - # Try to auto-fetch a Cloudflare Access token - TOKEN="" - if command -v cloudflared >/dev/null 2>&1; then - TOKEN=$(cloudflared access token --app="$URL" 2>/dev/null || true) + # cloudflared is required for URL shorthand + if ! command -v cloudflared >/dev/null 2>&1; then + echo "Error: cloudflared is required to connect to remote URLs." >&2 + echo "Install it: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" >&2 + exit 1 fi - if [ -n "$TOKEN" ]; then - exec bun run --cwd "$INSTALL_DIR/packages/opencode" dev -- attach "$URL" -H "cf-access-token: $TOKEN" "$@" - else - exec bun run --cwd "$INSTALL_DIR/packages/opencode" dev -- attach "$URL" "$@" - fi + # Fetch CF Access token (shows browser login link if needed) + TOKEN=$(cloudflared access token --app="$URL") + + exec bun run --cwd "$INSTALL_DIR/packages/opencode" dev -- attach "$URL" -H "cf-access-token: $TOKEN" "$@" fi # Otherwise pass through to opencode normally @@ -149,8 +149,8 @@ echo -e "${MUTED}Then verify:${NC}" echo -e " cfcode --version ${MUTED}# Should print 'local'${NC}" echo -e " cfcode attach -H \"cf-access-token: \"" echo "" -echo -e "${MUTED}Quick connect to a remote URL:${NC}" -echo -e " cfcode ${MUTED}# Attaches and auto-fetches CF Access token if available${NC}" +echo -e "${MUTED}Quick connect to a remote URL (requires cloudflared):${NC}" +echo -e " cfcode ${MUTED}# Auto-fetches CF Access token and attaches${NC}" echo "" echo -e "${MUTED}Example:${NC}" echo -e " cfcode https://4096-sb-867770819f83-j6kaxtmu0u7opzod.superseal.cloudflare.dev/" From 76a8516bcde000ce906a4dad259357487fb61815 Mon Sep 17 00:00:00 2001 From: "byule@cloudflare.com" Date: Fri, 1 May 2026 16:02:34 -0400 Subject: [PATCH 11/11] fix: use cloudflared access login to show browser link before fetching token --- install-dev.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install-dev.sh b/install-dev.sh index 06e15cebde10..f2eec94a9dee 100755 --- a/install-dev.sh +++ b/install-dev.sh @@ -84,7 +84,8 @@ if [ $# -gt 0 ] && [[ "${1:-}" =~ ^https?:// ]]; then exit 1 fi - # Fetch CF Access token (shows browser login link if needed) + # Ensure CF Access token is available (login if needed, shows browser link) + cloudflared access login "$URL" TOKEN=$(cloudflared access token --app="$URL") exec bun run --cwd "$INSTALL_DIR/packages/opencode" dev -- attach "$URL" -H "cf-access-token: $TOKEN" "$@"