Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
08dc6ec
Run freebuff Windows build + smoke on every push
jahooma May 4, 2026
6b3dcd1
Bump Freebuff version to 0.0.64
github-actions[bot] May 4, 2026
1b6333e
Add --smoke-tree-sitter flag and fail builds with empty embed
jahooma May 4, 2026
ad6a900
Bump version to 1.0.655
github-actions[bot] May 4, 2026
54c0729
Switch tree-sitter wasm embed from base64 string to `with { type: 'fi…
jahooma May 4, 2026
ecdb374
Bump version to 1.0.656
github-actions[bot] May 4, 2026
01fefda
Bump Freebuff version to 0.0.65
github-actions[bot] May 4, 2026
7d58294
Move --smoke-tree-sitter handler to main() to bypass commander
jahooma May 4, 2026
b1bd842
Bump version to 1.0.657
github-actions[bot] May 4, 2026
f9f207a
Stage tree-sitter.wasm into pre-init/ for relative `with { type: 'fil…
jahooma May 4, 2026
9b58574
Bump version to 1.0.658
github-actions[bot] May 4, 2026
e505cc7
Bump Freebuff version to 0.0.66
github-actions[bot] May 4, 2026
3ad502b
Embed tree-sitter wasm as ~268 chunked base64 string literals
jahooma May 4, 2026
38770b9
Bump version to 1.0.659
github-actions[bot] May 4, 2026
b0dc5de
Bump Freebuff version to 0.0.67
github-actions[bot] May 4, 2026
c8228e3
Export wasm chunks as a function so the bundler can't inline them away
jahooma May 4, 2026
bcf03ec
Bump version to 1.0.660
github-actions[bot] May 4, 2026
24346bc
Bump Freebuff version to 0.0.68
github-actions[bot] May 4, 2026
299a4df
Ship tree-sitter.wasm as a sibling file next to the CLI binary
jahooma May 4, 2026
a3cc430
Bump Freebuff version to 0.0.69
github-actions[bot] May 4, 2026
6256069
Bump version to 1.0.661
github-actions[bot] May 4, 2026
03a91ca
Diagnostic dump in --smoke-tree-sitter handler
jahooma May 4, 2026
510384e
Bump version to 1.0.662
github-actions[bot] May 4, 2026
d642f94
Bump Freebuff version to 0.0.70
github-actions[bot] May 4, 2026
09564b2
Use argv[0] (not execPath) to find sibling wasm — pre-init fix on Win…
jahooma May 4, 2026
177ca99
Bump version to 1.0.663
github-actions[bot] May 4, 2026
1ceaa13
Bump Freebuff version to 0.0.71
github-actions[bot] May 4, 2026
726c18e
Move sibling-wasm lookup from pre-init to init-node's locateFile call…
jahooma May 4, 2026
b2d8b92
Bump version to 1.0.664
github-actions[bot] May 4, 2026
9ba251b
Bump Freebuff version to 0.0.72
github-actions[bot] May 4, 2026
82a511c
Drop isBunEmbeddedPath shortcut — emscripten can't read those paths a…
jahooma May 4, 2026
31ce775
Bump version to 1.0.665
github-actions[bot] May 4, 2026
633cddd
Bump Freebuff version to 0.0.73
github-actions[bot] May 4, 2026
c77e79f
Smoke handler: also fall back to sibling-of-execPath lookup
jahooma May 4, 2026
0fbd844
Bump version to 1.0.666
github-actions[bot] May 4, 2026
86ebd09
Bump Freebuff version to 0.0.74
github-actions[bot] May 4, 2026
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
9 changes: 7 additions & 2 deletions .github/workflows/cli-release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,10 @@ jobs:
if [[ "${{ runner.os }}" == "Windows" ]]; then
BINARY_FILE="${{ inputs.binary-name }}.exe"
fi
tar -czf ${{ inputs.binary-name }}-${{ matrix.target }}.tar.gz -C cli/bin "$BINARY_FILE"
# Bundle the binary alongside tree-sitter.wasm — the CLI loads
# the wasm as a sibling file at runtime since bun --compile
# asset embedding wasn't reliable on Windows.
tar -czf ${{ inputs.binary-name }}-${{ matrix.target }}.tar.gz -C cli/bin "$BINARY_FILE" tree-sitter.wasm

- name: Upload binary artifact
uses: actions/upload-artifact@v7
Expand Down Expand Up @@ -340,7 +343,9 @@ jobs:
shell: bash
run: |
BINARY_FILE="${{ inputs.binary-name }}.exe"
tar -czf ${{ inputs.binary-name }}-win32-x64.tar.gz -C cli/bin "$BINARY_FILE"
# Bundle tree-sitter.wasm next to the binary; see the
# equivalent matrix-job tar step for context.
tar -czf ${{ inputs.binary-name }}-win32-x64.tar.gz -C cli/bin "$BINARY_FILE" tree-sitter.wasm

- name: Upload binary artifact
uses: actions/upload-artifact@v7
Expand Down
124 changes: 124 additions & 0 deletions .github/workflows/freebuff-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,130 @@ jobs:
path: cli/bin/freebuff
retention-days: 1

# Windows-native build + smoke. The full tmux-based e2e matrix below can't
# run here (Windows runners don't have tmux), but the smoke-binary.ts
# check is what would have caught the post-OpenTUI-upgrade tree-sitter
# wasm regression: that bug only manifested on real Windows, while CI was
# Linux-only and macOS dev machines saw it work. Now every push gets a
# real Windows boot test.
build-and-smoke-freebuff-windows:
runs-on: windows-latest
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v6

- uses: ./.github/actions/setup-project

- name: Ensure CLI dependencies
run: bun install --frozen-lockfile --cwd cli
shell: bash

# Mirror the symlink fix from cli-release-build.yml's Windows job: bun
# workspace symlinks aren't created reliably on Windows runners, so
# the cli's @opentui imports need explicit junctions to the root
# @opentui packages.
- name: Fix OpenTUI module symlinks
shell: bash
run: |
set -euo pipefail
bun - <<'BUN'
import fs from 'fs';
import path from 'path';

const rootDir = process.cwd();
const rootOpenTui = path.join(rootDir, 'node_modules', '@opentui');
const cliNodeModules = path.join(rootDir, 'cli', 'node_modules');
const cliOpenTui = path.join(cliNodeModules, '@opentui');

if (!fs.existsSync(rootOpenTui)) {
console.log('Root @opentui packages missing; skipping fix');
process.exit(0);
}

fs.mkdirSync(cliOpenTui, { recursive: true });

const packages = ['core', 'react'];
for (const pkg of packages) {
const target = path.join(rootOpenTui, pkg);
const link = path.join(cliOpenTui, pkg);

if (!fs.existsSync(target)) {
console.log(`Target ${target} missing; skipping ${pkg}`);
continue;
}

let linkStats = null;
try {
linkStats = fs.lstatSync(link);
} catch (error) {
if (error?.code !== 'ENOENT') {
throw error;
}
}

if (linkStats) {
let alreadyLinked = false;
try {
const actual = fs.realpathSync(link);
alreadyLinked = actual === target;
} catch {
// Broken symlink or unreadable target; we'll replace it.
}

if (alreadyLinked) {
continue;
}

fs.rmSync(link, { recursive: true, force: true });
}

const type = process.platform === 'win32' ? 'junction' : 'dir';
try {
fs.symlinkSync(target, link, type);
console.log(`Linked ${link} -> ${target}`);
} catch (error) {
if (error?.code === 'EEXIST') {
fs.rmSync(link, { recursive: true, force: true });
fs.symlinkSync(target, link, type);
console.log(`Re-linked ${link} -> ${target}`);
} else {
throw error;
}
}
}
BUN

- name: Set environment variables
env:
SECRETS_CONTEXT: ${{ toJSON(secrets) }}
shell: bash
run: |
VAR_NAMES=$(bun scripts/generate-ci-env.ts --scope client)
echo "$SECRETS_CONTEXT" | jq -r --argjson vars "$VAR_NAMES" '
to_entries | .[] | select(.key as $k | $vars | index($k)) | .key + "=" + .value
' >> $GITHUB_ENV
echo "FREEBUFF_MODE=true" >> $GITHUB_ENV
echo "NEXT_PUBLIC_CB_ENVIRONMENT=prod" >> $GITHUB_ENV
echo "CODEBUFF_GITHUB_ACTIONS=true" >> $GITHUB_ENV

- name: Build Freebuff binary
run: bun freebuff/cli/build.ts 0.0.0-e2e
shell: bash

- name: Smoke test binary
shell: bash
run: |
# --version exits via commander synchronously and won't see async
# startup failures (e.g. the Parser.init rejection from a broken
# tree-sitter wasm load).
./cli/bin/freebuff.exe --version
# Run for several seconds so unhandled rejections during module
# init have time to fire — the freebuff 0.0.62 wasm regression
# surfaced through the *late* renderer-cleanup handler, after the
# boot screen had rendered, so a too-short window can miss it.
bun cli/scripts/smoke-binary.ts cli/bin/freebuff.exe

e2e:
needs: build-freebuff
runs-on: ubuntu-latest
Expand Down
21 changes: 21 additions & 0 deletions cli/release/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,27 @@ async function downloadBinary(version) {
}
fs.renameSync(tempBinaryPath, CONFIG.binaryPath)

// Move tree-sitter.wasm next to the binary if the tarball included
// it. The CLI binary loads this at startup; embedding it inside the
// binary itself was unreliable on Windows (bun --compile asset
// bundling silently dropped or unbound it across several attempts),
// so we ship it as a sibling file instead. Older artifacts that
// pre-date this change won't have the wasm and will still install —
// they'll just hit the same crash they had before, which is fine.
const tempWasmPath = path.join(CONFIG.tempDownloadDir, 'tree-sitter.wasm')
if (fs.existsSync(tempWasmPath)) {
const targetWasmPath = path.join(
path.dirname(CONFIG.binaryPath),
'tree-sitter.wasm',
)
try {
if (fs.existsSync(targetWasmPath)) fs.unlinkSync(targetWasmPath)
} catch {
// best effort; rename below will surface the real error if it matters
}
fs.renameSync(tempWasmPath, targetWasmPath)
}

// Save version metadata for fast version checking
fs.writeFileSync(
CONFIG.metadataPath,
Expand Down
2 changes: 1 addition & 1 deletion cli/release/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codebuff",
"version": "1.0.654",
"version": "1.0.666",
"description": "AI coding agent",
"license": "MIT",
"bin": {
Expand Down
81 changes: 33 additions & 48 deletions cli/scripts/build-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,6 @@ async function main() {
patchOpenTuiAssetPaths()
await ensureOpenTuiNativeBundle(targetInfo)

const restoreTreeSitterWasmStub = embedTreeSitterWasmAsBase64()
// Restore the stub even on build failure so a developer's git working
// tree doesn't end up with a multi-megabyte modified file.
process.on('exit', restoreTreeSitterWasmStub)

const outputFilename =
targetInfo.platform === 'win32' ? `${binaryName}.exe` : binaryName
const outputFile = join(binDir, outputFilename)
Expand Down Expand Up @@ -191,10 +186,18 @@ async function main() {

runCommand('bun', buildArgs, { cwd: cliRoot })

// Build done — restore the stub so a developer's working tree doesn't show
// a multi-megabyte diff. (The exit handler above is a backstop for crashes;
// the eager call here keeps a successful build clean.)
restoreTreeSitterWasmStub()
// Ship tree-sitter.wasm as a sibling file next to the binary. Bun
// --compile asset embedding is unreliable on Windows (every JS-level
// retrieval mechanism we tried — `with { type: 'file' }`, base64 string
// literals, chunked base64, function-wrapped chunked base64 — got
// tree-shaken, minified away, or returned an undefined binding even
// when the bytes were in the binary). The pre-init reads it from
// `dirname(process.execPath)`, which works the same on every platform
// because it's a normal disk read, not a bunfs lookup.
const sourceWasm = findWebTreeSitterWasm()
const siblingWasm = join(binDir, 'tree-sitter.wasm')
writeFileSync(siblingWasm, readFileSync(sourceWasm))
logAlways(`Copied tree-sitter.wasm sibling: ${sourceWasm} → ${siblingWasm}`)

if (targetInfo.platform !== 'win32') {
chmodSync(outputFile, 0o755)
Expand All @@ -215,47 +218,29 @@ main().catch((error: unknown) => {
})

/**
* Inline the contents of `web-tree-sitter/tree-sitter.wasm` as a base64 string
* literal in `cli/src/pre-init/tree-sitter-wasm-bytes.ts`. The committed
* file is a stub; this overwrites it with the real bytes immediately before
* `bun build --compile`, so the bytes get baked into the binary's text
* segment instead of being placed at a bunfs path that has to be fs-read at
* runtime.
*
* Returns a function that restores the stub. Always invoke it (success or
* failure) so a developer's working tree doesn't show a multi-MB diff.
* Find web-tree-sitter's tree-sitter.wasm in any plausible node_modules
* layout — bun hoists differently across platforms and `bun install`
* variants, and CI Windows lays it out differently than monorepo-root
* installs.
*/
function embedTreeSitterWasmAsBase64(): () => void {
const stubPath = join(cliRoot, 'src', 'pre-init', 'tree-sitter-wasm-bytes.ts')
const originalStub = readFileSync(stubPath, 'utf8')
let restored = false
const restore = (): void => {
if (restored) return
restored = true
try {
writeFileSync(stubPath, originalStub)
} catch (error) {
console.error('Failed to restore tree-sitter-wasm-bytes stub:', error)
}
function findWebTreeSitterWasm(): string {
const candidates = [
join(cliRoot, 'node_modules', 'web-tree-sitter', 'tree-sitter.wasm'),
join(cliRoot, '..', 'node_modules', 'web-tree-sitter', 'tree-sitter.wasm'),
join(cliRoot, '..', 'sdk', 'node_modules', 'web-tree-sitter', 'tree-sitter.wasm'),
]
const found = candidates.find((p) => existsSync(p))
if (found) return found
try {
const cliRequire = createRequire(join(cliRoot, 'package.json'))
return cliRequire.resolve('web-tree-sitter/tree-sitter.wasm')
} catch (err) {
throw new Error(
`Could not locate web-tree-sitter/tree-sitter.wasm. Searched:\n - ` +
candidates.join('\n - ') +
`\nAnd createRequire failed: ${err instanceof Error ? err.message : String(err)}`,
)
}

// Resolve from the CLI workspace so monorepo hoisting differences don't
// matter — `web-tree-sitter` is an SDK dep, but the CLI imports it
// transitively and the bundler walks it from here.
const cliRequire = createRequire(join(cliRoot, 'package.json'))
const wasmPath = cliRequire.resolve('web-tree-sitter/tree-sitter.wasm')
const wasmBytes = readFileSync(wasmPath)
const base64 = wasmBytes.toString('base64')

const generated =
`// AUTO-GENERATED by cli/scripts/build-binary.ts during \`bun build --compile\`.\n` +
`// Restored to the empty stub after the build finishes — do not commit a\n` +
`// non-empty value here.\n` +
`export const TREE_SITTER_WASM_BASE64 = ${JSON.stringify(base64)}\n`

writeFileSync(stubPath, generated)
log(`Embedded tree-sitter.wasm (${wasmBytes.length} bytes → ${base64.length} chars base64)`)
return restore
}

function patchOpenTuiAssetPaths() {
Expand Down
36 changes: 36 additions & 0 deletions cli/scripts/smoke-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,39 @@ const FATAL_PATTERNS = [
// the renderer is up).
const DEFAULT_RUN_SECONDS = 10

function runTreeSitterSmoke(binary: string): Promise<void> {
return new Promise((resolve, reject) => {
const proc = spawn(binary, ['--smoke-tree-sitter'], {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
})

let captured = ''
const append = (chunk: Buffer): void => {
captured += chunk.toString('utf8')
}
proc.stdout?.on('data', append)
proc.stderr?.on('data', append)

proc.once('error', reject)
proc.once('exit', (code) => {
if (code === 0 && /tree-sitter smoke ok/.test(captured)) {
resolve()
return
}

reject(
new Error(
`tree-sitter smoke failed with exit code ${code}\n${captured.slice(
0,
8 * 1024,
)}`,
),
)
})
})
}

async function main(): Promise<void> {
const binary = process.argv[2]
const runSeconds = Number(process.argv[3] ?? DEFAULT_RUN_SECONDS)
Expand All @@ -100,6 +133,9 @@ async function main(): Promise<void> {

console.log(`smoke-binary: spawning ${binary} for ${runSeconds}s…`)

await runTreeSitterSmoke(binary)
console.log('smoke-binary: tree-sitter init OK.')

const proc = spawn(binary, [], {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
Expand Down
Loading
Loading