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 @@ -95,3 +95,4 @@ external/
/.flashgrep-index-engine/

.design/
.pnpm-store/
19 changes: 19 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 @@ -19,6 +19,7 @@ members = [
"src/crates/adapters/ai-adapters",
"src/crates/adapters/opencode-adapter",
"src/crates/adapters/dsh-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
4 changes: 2 additions & 2 deletions docs/architecture/extensions/plugin-runtime-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

| 名称 | 唯一含义 |
|---|---|
| Plugin Host | 运行 Node/Bun 与第三方 JS/TS 插件的受监督子进程;Host 不在 Rust 主应用进程内 |
| Plugin Host | 运行 Bun 与第三方 JS/TS 插件的受监督子进程;Host 不在 Rust 主应用进程内 |
| `PluginRuntimeClient` | Rust 主应用内部现有调用端口;校验请求和响应,管理超时、同一插件的串行调用、重复请求结果缓存、诊断与故障隔离 |
| `ScriptToolRuntime` / `NodeScriptToolRuntime` | 现有脚本执行端口及 services 实现;当前负责 standalone Tool worker,后续 Plugin Host 的物理进程职责也应沿此边界扩展 |
| 插件实例 | 由来源、插件身份和当前内容版本确定的已启用插件;启停事实仍由现有来源与能力模块管理 |
Expand All @@ -37,7 +37,7 @@ flowchart LR
Client["PluginRuntimeClient"]
Adapter["生态适配器"]
Service["Process service"]
Host["Plugin Host\nNode/Bun"]
Host["Plugin Host\nBun"]

Owners <--> Client
Client <--> Adapter
Expand Down
14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
"test:skin-market": "pnpm --dir src/skin-market-web test",
"prepare:mobile-web": "node scripts/mobile-web-build.cjs",
"prepare:dsh-profile": "node scripts/prepare-dsh-profile.mjs",
"plugin-host:install": "bun install --cwd src/apps/extension-host --frozen-lockfile",
"plugin-host:build": "bun run --cwd src/apps/extension-host build",
"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 @@ -98,11 +102,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
32 changes: 30 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
* 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,25 @@ 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')];
if (pluginHostEntries.some((entry) => !existsSync(entry))) {
errors.push({
name: 'OpenCode extension Host dist',
message:
'src/apps/extension-host/dist is missing the Bun 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 +136,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
66 changes: 59 additions & 7 deletions scripts/check-build-prereqs.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const scriptPath = path.join(repoRoot, 'scripts/check-build-prereqs.mjs');
function createTestRoot({
nodeModules = false,
mobileWebDist = false,
pluginHostDist = false,
sherpaOnnx = null,
} = {}) {
const root = mkdtempSync(path.join(tmpdir(), 'bitfun-build-prereqs-'));
Expand All @@ -29,6 +30,18 @@ function createTestRoot({
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'), '');
}

if (sherpaOnnx) {
for (const version of sherpaOnnx) {
const libDir = path.join(
Expand All @@ -48,21 +61,36 @@ function createTestRoot({

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', '');
}
`,
);
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 @@ -94,20 +122,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 @@ -122,6 +152,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 @@ -137,6 +168,7 @@ test('does not require the DeepSeek profile for cargo check', (t) => {
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 }));
Expand All @@ -148,8 +180,27 @@ test('does not require the DeepSeek profile for cargo check', (t) => {
assert.doesNotMatch(result.stderr, /prepare:dsh-profile/);
});

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 @@ -180,10 +231,11 @@ 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/);
assert.match(result.stdout, /\$ pnpm run plugin-host:prepare/);
assert.doesNotMatch(result.stdout, /prepare:dsh-profile/);
assert.match(result.stdout, /Re-checking prerequisites/);
assert.match(result.stdout, /All errors resolved/);
Expand Down
1 change: 1 addition & 0 deletions scripts/check-core-boundaries.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1458,6 +1458,7 @@ const CLI_REVIEWED_CORE_FEATURES = [
...ACP_REVIEWED_CORE_FEATURES,
'remote-connect',
'plugin-runtime',
'opencode-plugin-host',
];

const APP_SERVER_REVIEWED_CORE_FEATURES = [
Expand Down
28 changes: 27 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, 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,24 @@ 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'];

export function stagePluginHostResources(destination, sourceDirectory = PLUGIN_HOST_DIST) {
for (const entry of PLUGIN_HOST_ENTRIES) {
const source = join(sourceDirectory, 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(destination, { recursive: true });
for (const entry of PLUGIN_HOST_ENTRIES) {
copyFileSync(join(sourceDirectory, entry), join(destination, entry));
}
}

function stripDelimiter(args) {
const result = [...args];
Expand Down Expand Up @@ -76,6 +94,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 +117,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
Loading