Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,4 @@ external/
/.flashgrep-index-engine/

.design/
.pnpm-store/
18 changes: 18 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ members = [
"src/crates/assembly/external-sources",
"src/crates/adapters/ai-adapters",
"src/crates/adapters/opencode-adapter",
"src/crates/adapters/opencode-plugin-host",
"src/crates/adapters/claude-code-adapter",
"src/crates/adapters/codex-adapter",
"src/crates/adapters/static-hook-support",
Expand Down
14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
"type-check:skin-market": "pnpm --dir src/skin-market-web type-check",
"test:skin-market": "pnpm --dir src/skin-market-web test",
"prepare:mobile-web": "node scripts/mobile-web-build.cjs",
"plugin-host:install": "bun install --cwd src/apps/extension-host --frozen-lockfile",
"plugin-host:build": "bun run --cwd src/apps/extension-host build:all",
"plugin-host:prepare": "pnpm run plugin-host:install && pnpm run plugin-host:build",
"plugin-host:test": "bun test --cwd src/apps/extension-host",
"frontend:build-all": "node scripts/frontend-build-all.mjs",
"preview": "pnpm --dir src/web-ui preview",
"desktop:dev": "node scripts/dev.cjs desktop",
Expand All @@ -91,11 +95,11 @@
"installer:build:only": "pnpm --dir BitFun-Installer run installer:build:only",
"installer:build:only:fast": "pnpm --dir BitFun-Installer run installer:build:only:fast",
"installer:dev": "pnpm --dir BitFun-Installer run installer:dev",
"cli:dev": "node scripts/cli-product.mjs dev",
"cli:build": "node scripts/cli-product.mjs build",
"cli:install": "node scripts/install-cli.mjs",
"cli:install:unix": "bash src/apps/cli/install.sh",
"cli:install:windows": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File src/apps/cli/install.ps1",
"cli:dev": "pnpm run plugin-host:prepare && node scripts/cli-product.mjs dev",
"cli:build": "pnpm run plugin-host:prepare && node scripts/cli-product.mjs build",
"cli:install": "pnpm run plugin-host:prepare && node scripts/install-cli.mjs",
"cli:install:unix": "pnpm run plugin-host:prepare && bash src/apps/cli/install.sh",
"cli:install:windows": "pnpm run plugin-host:prepare && powershell.exe -NoProfile -ExecutionPolicy Bypass -File src/apps/cli/install.ps1",
"cli:run": "cd src/apps/cli && cargo run --release --",
"cli:exec": "cd src/apps/cli && cargo run -- exec",
"cli:check": "cd src/apps/cli && cargo check",
Expand Down
35 changes: 33 additions & 2 deletions scripts/check-build-prereqs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
* - src/mobile-web/dist missing → cargo check -p bitfun-desktop and
* cargo check --workspace fail with "resource path '../../mobile-web/dist'
* doesn't exist" in the bitfun-desktop build script
* - OpenCode extension Host dist missing → CLI builds cannot bundle the
* Node/Bun plugin Host resources
* - sherpa-onnx prebuilt libs missing → sherpa-onnx-sys build script attempts
* a network download from GitHub that fails on poor connectivity
*
Expand Down Expand Up @@ -57,7 +59,28 @@ function runChecks(rootDir) {
});
}

// --- Check 3: sherpa-onnx prebuilt libs ---
// --- Check 3: OpenCode extension Host dist (CLI bundled resource) ---
const pluginHostDist = join(
rootDir,
'src',
'apps',
'extension-host',
'dist',
);
const pluginHostEntries = [
join(pluginHostDist, 'extension-host.js'),
join(pluginHostDist, 'extension-host-node.js'),
];
if (pluginHostEntries.some((entry) => !existsSync(entry))) {
errors.push({
name: 'OpenCode extension Host dist',
message:
'src/apps/extension-host/dist is missing the Bun or Node Host entry. CLI builds bundle this directory as the plugin Host resource.',
fix: ['pnpm', 'run', 'plugin-host:prepare'],
});
}

// --- Check 4: sherpa-onnx prebuilt libs ---
// sherpa-onnx-sys build.rs auto-detects target/sherpa-onnx-prebuilt/<version>/lib/
// and returns immediately without downloading. Only warn for the first-build
// scenario where no prebuilt cache exists yet.
Expand Down Expand Up @@ -116,7 +139,15 @@ function runFixes(pendingFixes, rootDir) {
const [cmd, ...args] = fix;
console.log(`$ ${fix.join(' ')}`);
try {
execFileSync(cmd, args, { stdio: 'inherit', cwd: rootDir });
if (process.platform === 'win32') {
execFileSync(
process.env.ComSpec || 'cmd.exe',
['/d', '/s', '/c', fix.join(' ')],
{ stdio: 'inherit', cwd: rootDir },
);
} else {
execFileSync(cmd, args, { stdio: 'inherit', cwd: rootDir });
}
} catch {
console.error(`Fix command failed: ${fix.join(' ')}\n`);
allSucceeded = false;
Expand Down
72 changes: 64 additions & 8 deletions scripts/check-build-prereqs.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ const repoRoot = path.resolve(
);
const scriptPath = path.join(repoRoot, 'scripts/check-build-prereqs.mjs');

function createTestRoot({ nodeModules = false, mobileWebDist = false, sherpaOnnx = null } = {}) {
function createTestRoot({
nodeModules = false,
mobileWebDist = false,
pluginHostDist = false,
sherpaOnnx = null,
} = {}) {
const root = mkdtempSync(path.join(tmpdir(), 'bitfun-build-prereqs-'));

if (nodeModules) {
Expand All @@ -25,6 +30,19 @@ function createTestRoot({ nodeModules = false, mobileWebDist = false, sherpaOnnx
writeFileSync(path.join(distDir, 'index.html'), '<html></html>');
}

if (pluginHostDist) {
const distDir = path.join(
root,
'src',
'apps',
'extension-host',
'dist',
);
mkdirSync(distDir, { recursive: true });
writeFileSync(path.join(distDir, 'extension-host.js'), '');
writeFileSync(path.join(distDir, 'extension-host-node.js'), '');
}

if (sherpaOnnx) {
for (const version of sherpaOnnx) {
const libDir = path.join(
Expand All @@ -44,21 +62,37 @@ function createTestRoot({ nodeModules = false, mobileWebDist = false, sherpaOnnx

function createFakePnpm() {
const binDir = mkdtempSync(path.join(tmpdir(), 'bitfun-fake-pnpm-'));
const pnpmPath = path.join(binDir, 'pnpm');
const fakePnpmPath = path.join(binDir, 'fake-pnpm.cjs');
writeFileSync(
pnpmPath,
`#!/usr/bin/env node
fakePnpmPath,
`
const { mkdirSync, writeFileSync } = require('fs');
const args = process.argv.slice(2);
if (args[0] === 'install') {
mkdirSync('node_modules', { recursive: true });
} else if (args[0] === 'run' && args[1] === 'prepare:mobile-web') {
mkdirSync('src/mobile-web/dist', { recursive: true });
writeFileSync('src/mobile-web/dist/index.html', '<html></html>');
} else if (args[0] === 'run' && args[1] === 'plugin-host:prepare') {
mkdirSync('src/apps/extension-host/dist', { recursive: true });
writeFileSync('src/apps/extension-host/dist/extension-host.js', '');
writeFileSync('src/apps/extension-host/dist/extension-host-node.js', '');
}
`,
);
chmodSync(pnpmPath, 0o755);
if (process.platform === 'win32') {
writeFileSync(
path.join(binDir, 'pnpm.cmd'),
`@echo off\r\n"${process.execPath}" "%~dp0fake-pnpm.cjs" %*\r\n`,
);
} else {
const pnpmPath = path.join(binDir, 'pnpm');
writeFileSync(
pnpmPath,
`#!/usr/bin/env node\nrequire('./fake-pnpm.cjs');\n`,
);
chmodSync(pnpmPath, 0o755);
}
return binDir;
}

Expand Down Expand Up @@ -90,20 +124,22 @@ test('passes when all prerequisites are present (including sherpa-onnx prebuilt)
const root = createTestRoot({
nodeModules: true,
mobileWebDist: true,
pluginHostDist: true,
sherpaOnnx: ['sherpa-onnx-v1.13.4-osx-arm64-static-lib'],
});
t.after(() => rmSync(root, { recursive: true, force: true }));

const result = runCheck(root, { sherpaEnv: '' });

assert.equal(result.status, 0);
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /Build prerequisite check passed/);
assert.doesNotMatch(result.stderr, /\[WARN\]/);
});

test('fails when root node_modules is missing', (t) => {
const root = createTestRoot({
mobileWebDist: true,
pluginHostDist: true,
sherpaOnnx: ['sherpa-onnx-v1.13.4-osx-arm64-static-lib'],
});
t.after(() => rmSync(root, { recursive: true, force: true }));
Expand All @@ -118,6 +154,7 @@ test('fails when root node_modules is missing', (t) => {
test('fails when mobile-web dist is missing', (t) => {
const root = createTestRoot({
nodeModules: true,
pluginHostDist: true,
sherpaOnnx: ['sherpa-onnx-v1.13.4-osx-arm64-static-lib'],
});
t.after(() => rmSync(root, { recursive: true, force: true }));
Expand All @@ -129,8 +166,27 @@ test('fails when mobile-web dist is missing', (t) => {
assert.match(result.stderr, /Fix: pnpm run prepare:mobile-web/);
});

test('fails when OpenCode extension Host dist is missing', (t) => {
const root = createTestRoot({
nodeModules: true,
mobileWebDist: true,
sherpaOnnx: ['sherpa-onnx-v1.13.4-osx-arm64-static-lib'],
});
t.after(() => rmSync(root, { recursive: true, force: true }));

const result = runCheck(root, { sherpaEnv: '' });

assert.notEqual(result.status, 0);
assert.match(result.stderr, /\[FAIL\] OpenCode extension Host dist/);
assert.match(result.stderr, /Fix: pnpm run plugin-host:prepare/);
});

test('warns when sherpa-onnx prebuilt dir does not exist (first build)', (t) => {
const root = createTestRoot({ nodeModules: true, mobileWebDist: true });
const root = createTestRoot({
nodeModules: true,
mobileWebDist: true,
pluginHostDist: true,
});
t.after(() => rmSync(root, { recursive: true, force: true }));

const result = runCheck(root, { sherpaEnv: '' });
Expand Down Expand Up @@ -161,7 +217,7 @@ test('--fix runs fix commands, re-verifies, and exits 0 when errors resolved', (

const result = runCheck(root, { fix: true, extraPath: binDir, sherpaEnv: '' });

assert.equal(result.status, 0);
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.match(result.stdout, /Attempting fixes/);
assert.match(result.stdout, /\$ pnpm install/);
assert.match(result.stdout, /\$ pnpm run prepare:mobile-web/);
Expand Down
26 changes: 25 additions & 1 deletion scripts/cli-product.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
Expand All @@ -9,6 +9,22 @@ import { ensureProductOutputDirectory, productBuildEnvironment } from './product
import { ProductDefinitionError, resolveProductDefinition } from './product-customization/resolver.mjs';

const ROOT = resolve(import.meta.dirname, '..');
const PLUGIN_HOST_DIST = join(ROOT, 'src', 'apps', 'extension-host', 'dist');
const PLUGIN_HOST_ENTRIES = ['extension-host.js', 'extension-host-node.js'];

function stagePluginHostResources(destination) {
for (const entry of PLUGIN_HOST_ENTRIES) {
const source = join(PLUGIN_HOST_DIST, entry);
if (!existsSync(source)) {
throw new Error(
`CLI plugin Host resource was not produced: ${source}. Run pnpm run plugin-host:prepare.`,
);
}
}
rmSync(destination, { recursive: true, force: true });
mkdirSync(join(destination, '..'), { recursive: true });
cpSync(PLUGIN_HOST_DIST, destination, { recursive: true });
}

function stripDelimiter(args) {
const result = [...args];
Expand Down Expand Up @@ -76,6 +92,12 @@ export function cliBuildPlan(resolution, mode, forwardArgs = [], platform = proc
cargoArgs,
internalBinaryPath: join(cargoTargetDir, ...(target ? [target] : []), profileDir, `bitfun${suffix}`),
stagedBinaryPath: join(resolution.outputDir, 'package', `${resolution.assembly.binaryName}${suffix}`),
stagedPluginHostPath: join(
resolution.outputDir,
'package',
'resources',
'ext-host',
),
};
}

Expand All @@ -93,7 +115,9 @@ function run(plan) {
ensureProductOutputDirectory(plan.resolution);
mkdirSync(join(plan.stagedBinaryPath, '..'), { recursive: true });
copyFileSync(plan.internalBinaryPath, plan.stagedBinaryPath);
stagePluginHostResources(plan.stagedPluginHostPath);
console.log(`[product] staged CLI: ${plan.stagedBinaryPath}`);
console.log(`[product] staged plugin Host: ${plan.stagedPluginHostPath}`);
}
}

Expand Down
1 change: 1 addition & 0 deletions scripts/cli-product.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ test('CLI uses the shared resolver and stages the internal binary under the memb
assert.ok(plan.cargoArgs.includes('--locked'));
assert.ok(plan.internalBinaryPath.endsWith('bitfun.exe'));
assert.ok(plan.stagedBinaryPath.endsWith('acme.exe'));
assert.ok(plan.stagedPluginHostPath.endsWith(join('resources', 'ext-host')));
assert.equal(plan.environment.BITFUN_PRODUCT_DISPLAY_NAME, 'Acme CLI');
});

Expand Down
16 changes: 16 additions & 0 deletions scripts/cli/package-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ for (const packageScript of [
assert.match(content, /THIRD_PARTY_NOTICES\.md/);
assert.match(content, /models-dev\.LICENSE\.txt/);
assert.match(content, /models-dev\.provenance\.json/);
assert.match(content, /extension-host/);
assert.match(content, /resources[\\/]ext-host/);
assert.match(content, /extension-host\.js/);
assert.match(content, /extension-host-node\.js/);
}

for (const workflow of [
'.github/workflows/cli-package.yml',
'.github/workflows/cli-package-manual.yml',
'.github/workflows/linux-binaries.yml',
'.github/workflows/nightly.yml',
]) {
const content = read(workflow);
assert.match(content, /oven-sh\/setup-bun@v2/);
assert.match(content, /plugin Host resources/);
assert.match(content, /plugin-host:prepare|extension-host/);
}

for (const workflow of [
Expand Down
Loading