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
58 changes: 58 additions & 0 deletions .github/workflows/deploy-connect.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Deploy Connect

on:
push:
branches:
- main
paths:
- "apps/connect/**"
- "packages/connect-db/**"
- "packages/tunnel-contract/**"
workflow_dispatch:

env:
NODE_VERSION: "22.x"
PNPM_VERSION: "9.15.0"

permissions:
contents: read

concurrency:
group: deploy-connect-${{ github.ref }}
cancel-in-progress: true

jobs:
deploy:
name: Deploy tunnel gate to Cloudflare Workers
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
run_install: false

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline

# No build step: the gate is a single worker entry (src/worker.ts) and
# wrangler's esbuild bundles its workspace deps (@bb/connect-db,
# @bb/tunnel-contract) straight from source. Plain `wrangler deploy`
# uses the top-level (production) config -> the bb-connect worker on
# *.getbb.app. BETTER_AUTH_SECRET is set once by hand (it must equal
# bb-web's) and is intentionally NOT synced from CI.
- name: Deploy tunnel gate
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: pnpm --filter @bb/connect exec wrangler deploy
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
name: Deploy Landing
name: Deploy Web

on:
push:
branches:
- main
paths:
- "apps/landing/**"
- "apps/web/**"
- "packages/connect-db/**"
workflow_dispatch:

env:
Expand All @@ -16,12 +17,12 @@ permissions:
contents: read

concurrency:
group: deploy-landing-${{ github.ref }}
group: deploy-web-${{ github.ref }}
cancel-in-progress: true

jobs:
deploy:
name: Deploy landing to Cloudflare Workers
name: Deploy getbb.app site to Cloudflare Workers
runs-on: ubuntu-latest
timeout-minutes: 20

Expand All @@ -45,42 +46,48 @@ jobs:
uses: actions/cache@v4
with:
path: .turbo/cache
key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-landing-turbo-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }}
key: ${{ runner.os }}-node-${{ env.NODE_VERSION }}-web-turbo-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-node-${{ env.NODE_VERSION }}-landing-turbo-${{ hashFiles('pnpm-lock.yaml') }}-
${{ runner.os }}-node-${{ env.NODE_VERSION }}-landing-turbo-
${{ runner.os }}-node-${{ env.NODE_VERSION }}-web-turbo-${{ hashFiles('pnpm-lock.yaml') }}-
${{ runner.os }}-node-${{ env.NODE_VERSION }}-web-turbo-

- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline

- name: Build landing
# CLOUDFLARE_ENV selects the wrangler env at BUILD time (the
# @cloudflare/vite-plugin writes the resolved production config to
# dist/server/wrangler.json; plain `wrangler deploy` follows it).
- name: Build web (production)
env:
CLOUDFLARE_ENV: production
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
run: pnpm exec turbo run build --filter=@bb/landing --cache-dir=.turbo/cache --output-logs=new-only
run: pnpm exec turbo run build --filter=@bb/web --cache-dir=.turbo/cache --output-logs=new-only

- name: Sync landing analytics secret
- name: Sync marketing analytics secret
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
LANDING_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
run: |
if [ -z "$LANDING_POSTHOG_KEY" ]; then
echo "VITE_POSTHOG_KEY is required for landing download tracking."
echo "VITE_POSTHOG_KEY is required for download tracking."
exit 1
fi
printf '%s' "$LANDING_POSTHOG_KEY" | pnpm --filter @bb/landing exec wrangler secret put LANDING_POSTHOG_KEY
printf '%s' "$LANDING_POSTHOG_KEY" | pnpm --filter @bb/web exec wrangler secret put LANDING_POSTHOG_KEY --name bb-web

- name: Sync landing email signup secret
- name: Sync email signup secret
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
run: |
if [ -z "$RESEND_API_KEY" ]; then
echo "RESEND_API_KEY is required for the landing email signup."
echo "RESEND_API_KEY is required for the email signup."
exit 1
fi
printf '%s' "$RESEND_API_KEY" | pnpm --filter @bb/landing exec wrangler secret put RESEND_API_KEY
printf '%s' "$RESEND_API_KEY" | pnpm --filter @bb/web exec wrangler secret put RESEND_API_KEY --name bb-web

- name: Deploy landing
# GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET / BETTER_AUTH_SECRET are set
# once by hand and intentionally NOT synced from CI.
- name: Deploy web
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: pnpm --filter @bb/landing run deploy
run: pnpm --filter @bb/web exec wrangler deploy
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
.dev.vars

# cloudflare wrangler local state
.wrangler/
!.env.example

# vercel
Expand Down
1 change: 1 addition & 0 deletions apps/app/src/lib/system-config-atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { wsManager } from "./ws";
const unavailableSystemConfig: SystemConfigResponse = {
experiments: {
claudeCodeMockCliTraffic: false,
multiMachine: false,
popoutChat: false,
popoutChatHotkey: "Alt+Space",
plugins: false,
Expand Down
7 changes: 7 additions & 0 deletions apps/app/src/views/SettingsView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@ function ExperimentsStory({
popoutChatHotkey,
}))
}
onMultiMachineEnabledChange={(enabled) =>
state.setExperiments((current) => ({
...current,
multiMachine: enabled,
}))
}
onPluginsEnabledChange={(enabled) =>
state.setExperiments((current) => ({
...current,
Expand All @@ -262,6 +268,7 @@ function ExperimentsStory({
uiForking: enabled,
}))
}
multiMachineEnabled={state.experiments.multiMachine}
pluginsEnabled={state.experiments.plugins}
popoutChatEnabled={state.experiments.popoutChat}
popoutChatHotkey={state.experiments.popoutChatHotkey}
Expand Down
24 changes: 24 additions & 0 deletions apps/app/src/views/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ export interface ExperimentsSettingsSectionProps {
disabled: boolean;
claudeCodeMockCliTrafficEnabled: boolean;
desktopShellAvailable: boolean;
multiMachineEnabled: boolean;
onClaudeCodeMockCliTrafficEnabledChange: (enabled: boolean) => void;
onMultiMachineEnabledChange: (enabled: boolean) => void;
onPopoutChatEnabledChange: (enabled: boolean) => void;
onPopoutChatHotkeyChange: (hotkey: string) => void;
onPluginsEnabledChange: (enabled: boolean) => void;
Expand Down Expand Up @@ -668,6 +670,7 @@ export function GeneralSettingsSection({
}

const CLAUDE_CODE_MOCK_CLI_TRAFFIC_EXPERIMENT_LABEL = "Mock CLI Traffic";
const MULTI_MACHINE_EXPERIMENT_LABEL = "Multi-machine";
const POPOUT_CHAT_EXPERIMENT_LABEL = "Popout chat";
const POPOUT_CHAT_HOTKEY_LABEL = "Hotkey";
const PLUGINS_EXPERIMENT_LABEL = "Plugins";
Expand Down Expand Up @@ -852,7 +855,9 @@ export function ExperimentsSettingsSection({
claudeCodeMockCliTrafficEnabled,
desktopShellAvailable,
disabled,
multiMachineEnabled,
onClaudeCodeMockCliTrafficEnabledChange,
onMultiMachineEnabledChange,
onPluginsEnabledChange,
onPopoutChatEnabledChange,
onPopoutChatHotkeyChange,
Expand Down Expand Up @@ -881,6 +886,18 @@ export function ExperimentsSettingsSection({
/>
</SettingsWithControl>

<SettingsWithControl
label={MULTI_MACHINE_EXPERIMENT_LABEL}
description="Run threads on other connected machines (bb thread spawn --host) and bb connect remote access. Off rejects execution on any host but this machine's."
>
<Switch
checked={multiMachineEnabled}
disabled={disabled}
onCheckedChange={onMultiMachineEnabledChange}
aria-label={MULTI_MACHINE_EXPERIMENT_LABEL}
/>
</SettingsWithControl>

<SettingsWithControl
label={PLUGINS_EXPERIMENT_LABEL}
description="Enable BB plugins (bb plugin). Plugins are full-trust code that extends the server and UI. This feature is unstable, and plugins might break in the future. Off unloads all plugin code."
Expand Down Expand Up @@ -1049,6 +1066,12 @@ export function SettingsView() {
popoutChatHotkey: hotkey,
})
}
onMultiMachineEnabledChange={(enabled) =>
updateExperimentsMutation.mutate({
...experiments,
multiMachine: enabled,
})
}
onPluginsEnabledChange={(enabled) =>
updateExperimentsMutation.mutate({
...experiments,
Expand All @@ -1061,6 +1084,7 @@ export function SettingsView() {
uiForking: enabled,
})
}
multiMachineEnabled={experiments.multiMachine}
pluginsEnabled={experiments.plugins}
popoutChatEnabled={experiments.popoutChat}
popoutChatHotkey={experiments.popoutChatHotkey}
Expand Down
69 changes: 69 additions & 0 deletions apps/cli/src/__tests__/command-output/host-list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, it, vi } from "vitest";
import type { Host } from "@bb/domain";
import {
setupCommandOutputTestEnvironment,
collectLogPayloads,
runCommand,
stubServerApi,
} from "../helpers/command-output-harness.js";
import type { CommandRegistrar } from "../helpers/command-output-harness.js";
import { registerHostCommands } from "../../commands/host.js";

function makeHost(overrides: Partial<Host> & Pick<Host, "id">): Host {
return {
name: overrides.id,
type: "persistent",
status: "connected",
lastSeenAt: null,
createdAt: 1,
updatedAt: 1,
...overrides,
};
}

describe("bb host list command output", () => {
setupCommandOutputTestEnvironment();

const register: CommandRegistrar = (program) =>
registerHostCommands(program, () => "http://server");

it("bb host list renders id, name, and status in the shared borderless table", async () => {
const list = vi.fn(async () => [
makeHost({ id: "host-primary-1", name: "studio", status: "connected" }),
makeHost({
id: "host-laptop-2",
name: "laptop",
status: "disconnected",
}),
]);
stubServerApi({ "v1.hosts.$get": list });

await runCommand(["host", "list"], register);

const output = collectLogPayloads(vi.mocked(console.log)).join("\n");
expect(output).toContain("ID Name Status Last seen");
expect(output).toContain("host-primary-1 studio connected -");
expect(output).toContain("host-laptop-2 laptop disconnected -");
});

it("bb host list prints a message when there are no hosts", async () => {
stubServerApi({ "v1.hosts.$get": vi.fn(async () => []) });

await runCommand(["host", "list"], register);

expect(collectLogPayloads(vi.mocked(console.log))).toEqual([
"No hosts found",
]);
});

it("bb host list --json prints the raw host list", async () => {
const hosts = [makeHost({ id: "host-primary-1", lastSeenAt: 1000 })];
stubServerApi({ "v1.hosts.$get": vi.fn(async () => hosts) });

await runCommand(["host", "list", "--json"], register);

const payloads = collectLogPayloads(vi.mocked(console.log));
expect(payloads).toHaveLength(1);
expect(JSON.parse(payloads[0])).toEqual(hosts);
});
});
2 changes: 2 additions & 0 deletions apps/cli/src/__tests__/json-flag-enforcement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { Command } from "commander";

import { registerStatusCommand } from "../commands/status.js";
import { registerHostCommands } from "../commands/host.js";
import { registerProjectCommands } from "../commands/project.js";
import { registerProviderCommands } from "../commands/provider.js";
import { registerManagerCommands } from "../commands/manager.js";
Expand Down Expand Up @@ -33,6 +34,7 @@ describe("CLI --json flag enforcement", () => {
const getUrl = () => "http://localhost";

registerStatusCommand(program, getUrl);
registerHostCommands(program, getUrl);
registerProjectCommands(program, getUrl);
registerProviderCommands(program, getUrl);
registerManagerCommands(program, getUrl);
Expand Down
Loading
Loading