Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-pandas-scan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"crosscode": patch
---

Fix Apple Terminal QR codes failing to scan. Replace half-block glyph rendering with full-cell ANSI background rendering plus spec quiet zone, so the code scans regardless of terminal line spacing, font, or light/dark theme. Adds `--no-qr` and `--small-qr` options.
2 changes: 2 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ crosscode -h
|---|---|
| `--cloudflared` | Force Cloudflare tunnel (`cloudflared` binary required) |
| `--ngrok` | Force ngrok tunnel (`ngrok` binary + auth token required) |
| `--no-qr` | Skip QR graphic, print URL only (useful when piping output) |
| `--small-qr` | Compact QR graphic with smaller quiet zone (same scannable full-cell rendering) |
| `--help`, `-h` | Show help message |

## Interactive Controls
Expand Down
4 changes: 2 additions & 2 deletions packages/crosscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"devDependencies": {
"@types/cross-spawn": "^6.0.6",
"@types/node": "^26.0.0",
"@types/qrcode-terminal": "^0.12.2",
"@types/qrcode": "^1.5.6",
"@types/ws": "^8.5.13",
"tsup": "^8.5.1",
"typescript": "^5.4.0"
Expand All @@ -36,7 +36,7 @@
"chalk": "^5.6.2",
"cross-spawn": "^7.0.6",
"ora": "^9.4.1",
"qrcode-terminal": "^0.12.0",
"qrcode": "^1.5.4",
"ws": "^8.18.0"
}
}
18 changes: 8 additions & 10 deletions packages/crosscode/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

import { readFile } from "fs/promises"
import chalk from "chalk"
import qrcode from "qrcode-terminal"
import { encodeQrPayload } from "@crosscode/shared"
import { debug, checkDep, setLogWriter, getFreePort, terminateChild } from "./util"
import { logCrosscode, closeAllLogs, crosscodeLogFile, cloudflaredLogFile, opencodeLogFile } from "./log"
import { readConfig, saveConfig, getProjectConfig, ensureSessionToken, resolveOpencodeBin } from "./config"
import { initSentry, captureCliError } from "./sentry"
import * as Sentry from "@sentry/node"
import { loginFlow, refreshTier } from "./auth"
import { printTunnelQr as renderTunnelQr } from "./qr"
import { proxyAgent } from "./proxy"
import { onKeypress, cleanupKeypress } from "./keypress"
import { startTunnelProvider } from "./providers/tunnel"
Expand All @@ -22,14 +21,11 @@ let isShuttingDown = false
setLogWriter(logCrosscode)

function printTunnelQr(url: string, token: string, opencodePort: number, requestedPort: number) {
const payload = encodeQrPayload({ url, token, v: 1 })
if (opencodePort !== requestedPort) {
console.log(chalk.yellow(`opencode running on port ${opencodePort}`))
}
console.log(chalk.cyanBright("\n Scan with CrossCode App:"))
qrcode.generate(payload, { small: true })
console.log(chalk.grey(`URL: ${url}`))
console.log(chalk.dim.bold("[Press 'l' for logs • 'h' for help • Ctrl+C to exit]"))
const argv = process.argv.slice(2)
renderTunnelQr(url, token, opencodePort, requestedPort, {
disabled: argv.includes("--no-qr"),
small: argv.includes("--small-qr") || process.env.CROSSCODE_QR_SMALL === "1",
})
}

function printHelp() {
Expand Down Expand Up @@ -105,6 +101,8 @@ ${chalk.yellow.bold("COMMANDS:")}
${chalk.yellow.bold("OPTIONS:")}
${chalk.green("--cloudflared")} Use Cloudflare tunnel (default for free tier)
${chalk.green("--ngrok")} Use ngrok tunnel (requires auth token)
${chalk.green("--no-qr")} Skip QR graphic, print URL only
${chalk.green("--small-qr")} Compact QR graphic (smaller margin)
${chalk.green("--help, -h")} Show this help message

${chalk.yellow.bold("EXAMPLES:")}
Expand Down
75 changes: 75 additions & 0 deletions packages/crosscode/src/qr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import chalk from "chalk"
import QRCode from "qrcode"
import { encodeQrPayload } from "@crosscode/shared"
import { debug } from "./util"

// Full-cell ANSI rendering: each QR module is one terminal cell painted
// with an explicit background colour (black/white). Unlike half-block
// glyph rendering (qrcode-terminal `small: true`, `▀▄█`), this does not
// depend on font glyph shapes, cell aspect ratio, light/dark theme, or
// Terminal.app line spacing — the gaps that made issue #133 unscannable.
const BLACK_CELL = "\x1b[40m \x1b[0m"
const WHITE_CELL = "\x1b[47m \x1b[0m"

export type QrRenderOptions = {
/** Compact but still full-cell output (smaller margin + lower EC). Default false. */
small?: boolean
}

export function renderQrTerminalText(data: string, opts: QrRenderOptions = {}): string {
const margin = opts.small ? 2 : 4
const errorCorrectionLevel = opts.small ? "L" : "M"
const qr = QRCode.create(data, { errorCorrectionLevel })
const size = qr.modules.size
const lines: string[] = []
for (let r = -margin; r < size + margin; r++) {
let line = ""
for (let c = -margin; c < size + margin; c++) {
const dark = r >= 0 && r < size && c >= 0 && c < size ? qr.modules.get(r, c) !== 0 : false
line += dark ? BLACK_CELL : WHITE_CELL
}
lines.push(line)
}
return lines.join("\n")
}

export type PrintTunnelQrOptions = QrRenderOptions & {
/** When true, skip the QR graphic and print the URL only. */
disabled?: boolean
}

export function printTunnelQr(
url: string,
token: string,
opencodePort: number,
requestedPort: number,
opts: PrintTunnelQrOptions = {},
) {
const payload = encodeQrPayload({ url, token, v: 1 })
if (opencodePort !== requestedPort) {
console.log(chalk.yellow(`opencode running on port ${opencodePort}`))
}
if (opts.disabled) {
console.log(chalk.cyanBright("\n Scan with CrossCode App:"))
console.log(chalk.grey(`URL: ${url}`))
console.log(chalk.dim("QR display disabled (--no-qr). Enter the URL manually in the app."))
console.log(chalk.dim.bold("[Press 'l' for logs • 'h' for help • Ctrl+C to exit]"))
return
}
console.log(chalk.cyanBright("\n Scan with CrossCode App:"))
try {
console.log()
// Raw ANSI string on its own line: never wrap in chalk or trim,
// or the explicit black/white cells lose contrast.
console.log(renderQrTerminalText(payload, { small: opts.small }))
console.log()
} catch (err) {
debug("QR render failed", { error: err instanceof Error ? err.message : String(err) })
console.log(chalk.yellow("Could not render QR code in this terminal."))
}
console.log(chalk.grey(`URL: ${url}`))
if (process.env.TERM_PROGRAM === "Apple_Terminal") {
console.log(chalk.dim("Terminal.app tip: if the code won't scan, zoom out (Cmd+-) and keep the whole code visible."))
}
console.log(chalk.dim.bold("[Press 'l' for logs • 'h' for help • Ctrl+C to exit]"))
}
35 changes: 13 additions & 22 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading