Compare commits

...

3 Commits

Author SHA1 Message Date
Nikhil Sonti
bc90766714 fix: bun.lock update 2026-03-26 17:34:40 -07:00
Nikhil Sonti
de50c5b378 fix: move cli install docs to top-level readme 2026-03-26 17:30:46 -07:00
Nikhil Sonti
ffe48948ff feat: add CDN upload flow for cli installers 2026-03-26 17:29:07 -07:00
10 changed files with 154 additions and 7 deletions

View File

@@ -43,6 +43,24 @@
4. Start automating!
## Install `browseros-cli`
Use `browseros-cli` when you want to control BrowserOS from the terminal or scripts via the BrowserOS MCP server.
### macOS / Linux
```bash
curl -fsSL https://cdn.browseros.com/cli/install.sh | bash
```
### Windows
```powershell
irm https://cdn.browseros.com/cli/install.ps1 | iex
```
After install, run `browseros-cli init` to point the CLI at your BrowserOS MCP server.
## What makes BrowserOS special
- 🏠 Feels like home — same Chrome interface, all your extensions just work
- 🤖 AI agents that run on YOUR browser, not in the cloud
@@ -164,4 +182,3 @@ Thank you to all our supporters!
Built with ❤️ from San Francisco
</p>

View File

@@ -0,0 +1,7 @@
# Production upload env for CLI installer scripts
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=browseros
R2_UPLOAD_PREFIX=cli

View File

@@ -2,12 +2,12 @@
# Install browseros-cli for Windows — downloads the latest release binary.
#
# Usage (PowerShell — save and run):
# Invoke-WebRequest -Uri "https://raw.githubusercontent.com/browseros-ai/BrowserOS/main/packages/browseros-agent/apps/cli/scripts/install.ps1" -OutFile install.ps1
# Invoke-WebRequest -Uri "https://cdn.browseros.com/cli/install.ps1" -OutFile install.ps1
# .\install.ps1
# .\install.ps1 -Version "0.1.0" -Dir "C:\tools\browseros"
#
# Usage (one-liner, uses env vars for options):
# & { $env:BROWSEROS_VERSION="0.1.0"; irm https://raw.githubusercontent.com/browseros-ai/BrowserOS/main/packages/browseros-agent/apps/cli/scripts/install.ps1 | iex }
# & { $env:BROWSEROS_VERSION="0.1.0"; irm https://cdn.browseros.com/cli/install.ps1 | iex }
#
param(

View File

@@ -3,10 +3,10 @@
# Install browseros-cli — downloads the latest release binary for your platform.
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/browseros-ai/BrowserOS/main/packages/browseros-agent/apps/cli/scripts/install.sh | bash
# curl -fsSL https://cdn.browseros.com/cli/install.sh | bash
#
# # Or with options:
# curl -fsSL ... | bash -s -- --version 0.1.0 --dir /usr/local/bin
# curl -fsSL https://cdn.browseros.com/cli/install.sh | bash -s -- --version 0.1.0 --dir /usr/local/bin
set -euo pipefail

View File

@@ -231,7 +231,7 @@
},
"packages/agent-sdk": {
"name": "@browseros-ai/agent-sdk",
"version": "0.0.5",
"version": "0.0.7",
"dependencies": {
"eventsource-parser": "^3.0.6",
"zod-to-json-schema": "^3.24.1",

View File

@@ -21,6 +21,7 @@
"build:server": "FORCE_COLOR=1 bun scripts/build/server.ts --target=all",
"build:server:ci": "FORCE_COLOR=1 bun scripts/build/server.ts --target=all --compile-only",
"build:server:test": "FORCE_COLOR=1 bun scripts/build/server.ts --target=darwin-arm64 --no-upload",
"upload:cli-installers": "bun scripts/build/cli.ts",
"start:server:test": "bun run build:server:test && set -a && . apps/server/.env.development && set +a && dist/prod/server/.tmp/binaries/browseros-server-darwin-arm64",
"build:agent:dev": "FORCE_COLOR=1 bun run --filter @browseros/agent --elide-lines=0 build:dev",
"build:agent": "bun run codegen:agent && bun run --filter @browseros/agent build",

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bun
import { runCliInstallerUpload } from './cli/upload'
runCliInstallerUpload().catch((error) => {
const message = error instanceof Error ? error.message : String(error)
console.error(`\n✗ ${message}\n`)
process.exit(1)
})

View File

@@ -0,0 +1,52 @@
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { parse } from 'dotenv'
import type { R2Config } from '../server/types'
const PROD_ENV_PATH = join('apps', 'cli', '.env.production')
const PROD_ENV_TEMPLATE_PATH = join('apps', 'cli', '.env.production.example')
function pickEnv(name: string, fileEnv: Record<string, string>): string {
const value = process.env[name] ?? fileEnv[name]
if (!value || value.trim().length === 0) {
throw new Error(`Missing required environment variable: ${name}`)
}
return value
}
function loadProdEnv(rootDir: string): Record<string, string> {
const prodEnvPath = join(rootDir, PROD_ENV_PATH)
if (!existsSync(prodEnvPath)) {
const templatePath = join(rootDir, PROD_ENV_TEMPLATE_PATH)
if (existsSync(templatePath)) {
throw new Error(
`Missing ${PROD_ENV_PATH}. Create it from ${PROD_ENV_TEMPLATE_PATH} before running upload:cli-installers.`,
)
}
throw new Error(
`Missing ${PROD_ENV_PATH}. The template file ${PROD_ENV_TEMPLATE_PATH} was not found.`,
)
}
return parse(readFileSync(prodEnvPath, 'utf-8'))
}
export interface CliUploadConfig {
r2: R2Config
}
export function loadCliUploadConfig(rootDir: string): CliUploadConfig {
const fileEnv = loadProdEnv(rootDir)
return {
r2: {
accountId: pickEnv('R2_ACCOUNT_ID', fileEnv),
accessKeyId: pickEnv('R2_ACCESS_KEY_ID', fileEnv),
secretAccessKey: pickEnv('R2_SECRET_ACCESS_KEY', fileEnv),
bucket: pickEnv('R2_BUCKET', fileEnv),
downloadPrefix: '',
uploadPrefix:
process.env.R2_UPLOAD_PREFIX ?? fileEnv.R2_UPLOAD_PREFIX ?? 'cli',
},
}
}

View File

@@ -0,0 +1,56 @@
import { existsSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { log } from '../log'
import { createR2Client, joinObjectKey, uploadFileToObject } from '../server/r2'
import { loadCliUploadConfig } from './config'
const CDN_BASE_URL = 'https://cdn.browseros.com'
const INSTALLERS = [
{
filePath: join('apps', 'cli', 'scripts', 'install.sh'),
objectName: 'install.sh',
contentType: 'text/x-shellscript; charset=utf-8',
},
{
filePath: join('apps', 'cli', 'scripts', 'install.ps1'),
objectName: 'install.ps1',
contentType: 'text/plain; charset=utf-8',
},
] as const
export async function runCliInstallerUpload(): Promise<void> {
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../../..')
process.chdir(rootDir)
await uploadCliInstallers(rootDir)
}
export async function uploadCliInstallers(rootDir: string): Promise<void> {
const { r2 } = loadCliUploadConfig(rootDir)
const client = createR2Client(r2)
log.header('Uploading BrowserOS CLI installer scripts')
try {
for (const installer of INSTALLERS) {
const absolutePath = join(rootDir, installer.filePath)
if (!existsSync(absolutePath)) {
throw new Error(`Installer script not found: ${installer.filePath}`)
}
const objectKey = joinObjectKey(r2.uploadPrefix, installer.objectName)
log.step(`Uploading ${installer.filePath}`)
await uploadFileToObject(client, r2, objectKey, absolutePath, {
contentType: installer.contentType,
})
log.success(`Uploaded ${objectKey}`)
log.info(`${CDN_BASE_URL}/${objectKey}`)
}
log.done('CLI installer upload completed')
} finally {
client.destroy()
}
}

View File

@@ -10,6 +10,10 @@ import {
import type { R2Config } from './types'
export interface UploadFileOptions {
contentType?: string
}
function createClientConfig(r2: R2Config): S3ClientConfig {
return {
region: 'auto',
@@ -81,6 +85,7 @@ export async function uploadFileToObject(
r2: R2Config,
key: string,
filePath: string,
options: UploadFileOptions = {},
): Promise<void> {
const data = await readFile(filePath)
await client.send(
@@ -88,7 +93,7 @@ export async function uploadFileToObject(
Bucket: r2.bucket,
Key: key,
Body: data,
ContentType: 'application/zip',
ContentType: options.contentType ?? 'application/zip',
}),
)
}