Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
32 changes: 10 additions & 22 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,13 @@ jobs:
gh release create "${tag}" --title "${tag}" --generate-notes --verify-tag
fi

# Binary/Homebrew release assets are intentionally disabled until binary
# releases are ready to ship. Re-enable these steps with the package scripts
# already kept in packages/cli/package.json:
#
# - name: Build Homebrew archive
# run: bun run --filter beeper-cli pack:homebrew
#
# - name: Publish GitHub release assets
# env:
# GH_TOKEN: ${{ github.token }}
# run: |
# set -euo pipefail
# tag="${GITHUB_REF_NAME}"
# if ! gh release view "${tag}" >/dev/null 2>&1; then
# gh release create "${tag}" --title "${tag}" --generate-notes --verify-tag
# fi
# gh release upload "${tag}" packages/cli/dist/bin/beeper-* packages/cli/dist/bin/binaries.json packages/cli/dist/release/*.tar.gz packages/cli/dist/release/homebrew.json --clobber
#
# - name: Publish Homebrew formula
# env:
# HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
# run: bun scripts/publish-homebrew-formula.ts
- name: Build binary release assets
run: bun run --filter beeper-cli binary

- name: Publish GitHub release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
gh release upload "${tag}" packages/cli/dist/bin/beeper-* packages/cli/dist/bin/binaries.json --clobber
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "desktop-api-cli-monorepo",
"name": "@beeper/cli",
"private": true,
"type": "module",
"packageManager": "bun@1.3.10",
Expand All @@ -16,6 +16,7 @@
"changeset": "changeset",
"lint": "eslint eslint.config.mjs packages/cli-plugin-cloudflare/src packages/cli-plugin-cloudflare/test",
"pack:packages": "mkdir -p .packs && (cd packages/cli && bun pm pack --destination ../../.packs) && (cd packages/cli-plugin-cloudflare && bun pm pack --destination ../../.packs)",
"publish:packages": "bun scripts/publish-packages.ts",
"release": "bun run check && bun changeset publish",
"test": "bun run --workspaces --sequential test",
"typecheck": "bun run --filter beeper-cli build && bun run --workspaces --sequential typecheck",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli-plugin-cloudflare/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@beeper/cli-plugin-cloudflare",
"version": "0.0.0",
"version": "0.6.0",
"description": "Cloudflare Tunnel commands for Beeper CLI",
"license": "MIT",
"type": "module",
Expand Down Expand Up @@ -34,7 +34,7 @@
},
"dependencies": {
"@oclif/core": "^4.11.2",
"beeper-cli": "workspace:*"
"beeper-cli": "^0.6.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli-plugin-cloudflare/src/lib/cloudflared.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { access, chmod, mkdir, rename, rm } from 'node:fs/promises'
import { access, chmod, mkdir, rename, rm, writeFile } from 'node:fs/promises'
import { arch, platform } from 'node:os'
import { basename, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
Expand Down Expand Up @@ -193,7 +193,7 @@ function downloadURL(system = platform(), cpu = arch()): string {
async function downloadFile(url: string, to: string): Promise<void> {
const response = await fetch(url, { redirect: 'follow' })
if (!response.ok || !response.body) throw new Error(`Could not download ${url}: ${response.status} ${response.statusText}`)
await Bun.write(to, response)
await writeFile(to, Buffer.from(await response.arrayBuffer()))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

export function findTunnelURL(data: string, domain = cloudflaredDomain()): string | undefined {
Expand Down
48 changes: 48 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ First-party optional plugins:
| `targets tunnel` | Expose a local Desktop API over a public Cloudflare tunnel |
| `auth status` | Show stored auth for the selected target |
| `auth logout` | Clear stored authentication |
| `auth email start` | Start email sign-in for a target |
| `auth email response` | Finish email sign-in with a verification code |
| `verify` | Finish setup verification or verify another device |
| `verify status` | Show encryption and device-verification readiness |
| `verify approve` | Approve a pending device verification request |
Expand Down Expand Up @@ -423,12 +425,14 @@ Flags:
| --- | --- | --- |
| `--channel=<stable|nightly>` | option | Install release channel Default: stable |
| `--desktop` | boolean | Set up a local Beeper Desktop target |
| `--email=<value>` | option | Sign in with an email address |
| `--install` | boolean | Allow installing missing managed runtime |
| `--local` | boolean | Use the local Beeper Desktop session on this device |
| `--oauth` | boolean | Authorize the target with browser OAuth/PKCE |
| `--remote=<value>` | option | Connect to a remote Beeper Desktop or Server URL |
| `--server` | boolean | Set up a local Beeper Server target |
| `--server-env=<production|staging>` | option | Server environment. Staging forces nightly. Default: production |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
| `--username=<value>` | option | Username to use if setup creates a new account |

Examples:

Expand Down Expand Up @@ -902,6 +906,50 @@ beeper auth logout

Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`.

### `beeper auth email start`
Start email sign-in for a target

```sh
beeper auth email start
```

Flags:

| Flag | Type | Description |
| --- | --- | --- |
| `--email=<value>` | option | Email address to sign in with Required. |

Examples:

```sh
beeper auth email start --email you@example.com --target work --json
```

Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`.

### `beeper auth email response`
Finish email sign-in with a verification code

```sh
beeper auth email response
```

Flags:

| Flag | Type | Description |
| --- | --- | --- |
| `--code=<value>` | option | Email verification code Required. |
| `--setup-request-id=<value>` | option | Setup request ID from auth email start Required. |
| `--username=<value>` | option | Username to use if setup creates a new account |

Examples:

```sh
beeper auth email response --setup-request-id <id> --code <code> --target work --json
```

Global flags: `--base-url`, `--target`, `--debug`, `--events`, `--full`, `--json`, `--quiet`, `--read-only`, `--timeout`, `--yes`.

### `beeper verify`
Finish setup verification or verify another device

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/bin/binary-bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ void (async () => {
const payloadHash = createHash('sha256').update(archive).digest('hex').slice(0, 16)
const cacheRoot = process.env.BEEPER_CLI_BINARY_CACHE_DIR || join(homedir(), '.cache', 'beeper-cli', 'binary')
const payloadRoot = join(cacheRoot, payloadHash)
const entrypoint = join(payloadRoot, 'bin', 'run.js')
const entrypoint = join(payloadRoot, 'bin', 'cli.js')

if (!existsSync(entrypoint)) {
const tempArchive = join(tmpdir(), `beeper-cli-${payloadHash}.tar.gz`)
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bun
import { execute } from '@oclif/core'
import { renderStartupLogo } from './logo.js'

void (async () => {
if (process.argv.slice(2).length === 0 && process.env.BEEPER_NO_LOGO !== '1') {
process.stdout.write(`${renderStartupLogo()}\n\n`)
}

await execute({ dir: import.meta.url })
})()
123 changes: 115 additions & 8 deletions packages/cli/bin/run.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,118 @@
#!/usr/bin/env bun
import { execute } from '@oclif/core'
import { renderStartupLogo } from './logo.js'
#!/usr/bin/env node
import { createHash } from 'node:crypto'
import { createWriteStream, existsSync } from 'node:fs'
import { chmod, mkdir, readFile, rename, rm } from 'node:fs/promises'
import { get } from 'node:https'
import { homedir, tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
import { pipeline } from 'node:stream/promises'
import { fileURLToPath } from 'node:url'
import { spawn } from 'node:child_process'

void (async () => {
if (process.argv.slice(2).length === 0 && process.env.BEEPER_NO_LOGO !== '1') {
process.stdout.write(`${renderStartupLogo()}\n\n`)
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
const pkg = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'))
const version = pkg.version
const platform = normalizePlatform(process.platform)
const arch = normalizeArch(process.arch)
const extension = platform === 'windows' ? '.exe' : ''
const executableName = `beeper-${platform}-${arch}${extension}`
const releaseTag = process.env.BEEPER_CLI_RELEASE_TAG || `v${version}`
const releaseBaseURL = (process.env.BEEPER_CLI_RELEASE_BASE_URL || `https://github.com/beeper/desktop-api-cli/releases/download/${releaseTag}`).replace(/\/$/, '')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const cacheRoot = process.env.BEEPER_CLI_BINARY_CACHE_DIR || join(homedir(), '.cache', 'beeper-cli')
const cacheDir = join(cacheRoot, version, `${platform}-${arch}`)
const cachedExecutable = join(cacheDir, platform === 'windows' ? 'beeper.exe' : 'beeper')

try {
const executable = await ensureExecutable()
const child = spawn(executable, process.argv.slice(2), {
env: process.env,
stdio: 'inherit',
})
child.once('exit', (code, signal) => {
if (signal) process.kill(process.pid, signal)
process.exit(code ?? 1)
})
child.once('error', error => {
console.error(`beeper-cli: failed to start downloaded binary: ${error.message}`)
process.exit(1)
})
} catch (error) {
console.error(`beeper-cli: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
}

async function ensureExecutable() {
if (existsSync(cachedExecutable)) return cachedExecutable

await mkdir(cacheDir, { recursive: true })
const tmpPath = join(tmpdir(), `${executableName}.${process.pid}.${Date.now()}.download`)
const url = `${releaseBaseURL}/${executableName}`
console.error(`beeper-cli: downloading ${url}`)
await download(url, tmpPath)

const expectedHash = await fetchExpectedHash().catch(() => undefined)
if (expectedHash) {
const actualHash = await sha256(tmpPath)
if (actualHash !== expectedHash) {
await rm(tmpPath, { force: true })
throw new Error(`downloaded binary checksum mismatch for ${executableName}`)
}
} else if (process.env.BEEPER_CLI_REQUIRE_CHECKSUM === '1') {
await rm(tmpPath, { force: true })
throw new Error(`no checksum found for ${executableName}`)
}

if (platform !== 'windows') await chmod(tmpPath, 0o755)
await rename(tmpPath, cachedExecutable)
return cachedExecutable
}

function normalizePlatform(value) {
if (value === 'darwin') return 'darwin'
if (value === 'linux') return 'linux'
if (value === 'win32') return 'windows'
throw new Error(`unsupported platform: ${value}`)
}

function normalizeArch(value) {
if (value === 'x64') return 'x64'
if (value === 'arm64') return 'arm64'
throw new Error(`unsupported architecture: ${value}`)
}

async function fetchExpectedHash() {
const manifestURL = `${releaseBaseURL}/binaries.json`
const manifestPath = join(tmpdir(), `beeper-cli-binaries-${version}-${process.pid}-${Date.now()}.json`)
try {
await download(manifestURL, manifestPath, { quiet: true })
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
return manifest.artifacts?.find(artifact => artifact.file === executableName)?.sha256
} finally {
await rm(manifestPath, { force: true })
}
}

async function download(url, destination, options = {}) {
await mkdir(dirname(destination), { recursive: true })
await new Promise((resolve, reject) => {
const request = get(url, response => {
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
response.resume()
download(new URL(response.headers.location, url).toString(), destination, options).then(resolve, reject)
return
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if (response.statusCode !== 200) {
response.resume()
reject(new Error(`download failed for ${basename(url)}: HTTP ${response.statusCode}`))
return
}
pipeline(response, createWriteStream(destination)).then(resolve, reject)
})
request.once('error', reject)
request.setTimeout(120_000, () => request.destroy(new Error(`download timed out: ${url}`)))
})
}

await execute({ dir: import.meta.url })
})()
async function sha256(path) {
return createHash('sha256').update(await readFile(path)).digest('hex')
}
5 changes: 3 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "MIT",
"type": "module",
"bin": {
"beeper": "./bin/run.js"
"beeper": "bin/run.js"
},
"exports": {
"./plugin-sdk": {
Expand All @@ -27,6 +27,7 @@
"check:readme": "bun run build && bun scripts/generate-readme.ts --check",
"clean": "rm -rf dist",
"dev": "bun ./bin/dev.js",
"dev:shim": "node ./bin/run.js",
"e2e:staging": "bun run build && bun test/e2e-staging.ts",
"pack:homebrew": "bun run binary && bun scripts/build-homebrew-archive.ts",
"readme": "bun run build && bun scripts/generate-readme.ts",
Expand All @@ -53,7 +54,7 @@
"scope": "beeper",
"pluginPrefix": "plugin",
"jitPlugins": {
"@beeper/cli-plugin-cloudflare": "^0.0.0"
"@beeper/cli-plugin-cloudflare": "^0.6.0"
},
"plugins": [
"@oclif/plugin-autocomplete",
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/scripts/build-binaries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bun
import { createHash } from 'node:crypto'
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { fileURLToPath } from 'node:url'

Expand Down Expand Up @@ -57,7 +58,7 @@ async function hashFile(path) {
}

async function buildPayload() {
const workDir = await mkdtemp(join('/private/tmp', 'beeper-cli-payload-'))
const workDir = await mkdtemp(join(tmpdir(), 'beeper-cli-payload-'))
try {
await cp(join(root, 'package.json'), join(workDir, 'package.json'))
await cp(join(root, 'bin'), join(workDir, 'bin'), { recursive: true })
Expand All @@ -79,7 +80,7 @@ async function buildPayload() {
async function run(command, args, options = {}) {
const child = Bun.spawn([command, ...args], {
cwd: options.cwd || root,
env: { ...process.env, TMPDIR: '/private/tmp' },
env: process.env,
stdin: 'inherit',
stdout: 'inherit',
stderr: 'inherit',
Expand Down
Loading