Skip to content

Commit 09a3376

Browse files
author
can2
committed
feat: integrate managed opencode plugin host
1 parent 92d06b8 commit 09a3376

104 files changed

Lines changed: 17638 additions & 143 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,4 @@ external/
9393
/.flashgrep-index-engine/
9494

9595
.design/
96+
.pnpm-store/

‎Cargo.lock‎

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ members = [
1818
"src/crates/assembly/external-sources",
1919
"src/crates/adapters/ai-adapters",
2020
"src/crates/adapters/opencode-adapter",
21+
"src/crates/adapters/opencode-plugin-host",
2122
"src/crates/adapters/claude-code-adapter",
2223
"src/crates/adapters/codex-adapter",
2324
"src/crates/adapters/static-hook-support",

‎package.json‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@
6565
"type-check:skin-market": "pnpm --dir src/skin-market-web type-check",
6666
"test:skin-market": "pnpm --dir src/skin-market-web test",
6767
"prepare:mobile-web": "node scripts/mobile-web-build.cjs",
68+
"plugin-host:install": "bun install --cwd src/apps/extension-host --frozen-lockfile",
69+
"plugin-host:build": "bun run --cwd src/apps/extension-host build",
70+
"plugin-host:prepare": "pnpm run plugin-host:install && pnpm run plugin-host:build",
71+
"plugin-host:test": "bun test --cwd src/apps/extension-host",
6872
"frontend:build-all": "node scripts/frontend-build-all.mjs",
6973
"preview": "pnpm --dir src/web-ui preview",
7074
"desktop:dev": "node scripts/dev.cjs desktop",
@@ -91,11 +95,11 @@
9195
"installer:build:only": "pnpm --dir BitFun-Installer run installer:build:only",
9296
"installer:build:only:fast": "pnpm --dir BitFun-Installer run installer:build:only:fast",
9397
"installer:dev": "pnpm --dir BitFun-Installer run installer:dev",
94-
"cli:dev": "node scripts/cli-product.mjs dev",
95-
"cli:build": "node scripts/cli-product.mjs build",
96-
"cli:install": "node scripts/install-cli.mjs",
97-
"cli:install:unix": "bash src/apps/cli/install.sh",
98-
"cli:install:windows": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File src/apps/cli/install.ps1",
98+
"cli:dev": "pnpm run plugin-host:prepare && node scripts/cli-product.mjs dev",
99+
"cli:build": "pnpm run plugin-host:prepare && node scripts/cli-product.mjs build",
100+
"cli:install": "pnpm run plugin-host:prepare && node scripts/install-cli.mjs",
101+
"cli:install:unix": "pnpm run plugin-host:prepare && bash src/apps/cli/install.sh",
102+
"cli:install:windows": "pnpm run plugin-host:prepare && powershell.exe -NoProfile -ExecutionPolicy Bypass -File src/apps/cli/install.ps1",
99103
"cli:run": "cd src/apps/cli && cargo run --release --",
100104
"cli:exec": "cd src/apps/cli && cargo run -- exec",
101105
"cli:check": "cd src/apps/cli && cargo check",

‎scripts/check-build-prereqs.mjs‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
* - src/mobile-web/dist missing → cargo check -p bitfun-desktop and
1111
* cargo check --workspace fail with "resource path '../../mobile-web/dist'
1212
* doesn't exist" in the bitfun-desktop build script
13+
* - OpenCode extension Host dist missing → CLI builds cannot bundle the
14+
* Bun plugin Host resources
1315
* - sherpa-onnx prebuilt libs missing → sherpa-onnx-sys build script attempts
1416
* a network download from GitHub that fails on poor connectivity
1517
*
@@ -57,7 +59,25 @@ function runChecks(rootDir) {
5759
});
5860
}
5961

60-
// --- Check 3: sherpa-onnx prebuilt libs ---
62+
// --- Check 3: OpenCode extension Host dist (CLI bundled resource) ---
63+
const pluginHostDist = join(
64+
rootDir,
65+
'src',
66+
'apps',
67+
'extension-host',
68+
'dist',
69+
);
70+
const pluginHostEntries = [join(pluginHostDist, 'extension-host.js')];
71+
if (pluginHostEntries.some((entry) => !existsSync(entry))) {
72+
errors.push({
73+
name: 'OpenCode extension Host dist',
74+
message:
75+
'src/apps/extension-host/dist is missing the Bun Host entry. CLI builds bundle this directory as the plugin Host resource.',
76+
fix: ['pnpm', 'run', 'plugin-host:prepare'],
77+
});
78+
}
79+
80+
// --- Check 4: sherpa-onnx prebuilt libs ---
6181
// sherpa-onnx-sys build.rs auto-detects target/sherpa-onnx-prebuilt/<version>/lib/
6282
// and returns immediately without downloading. Only warn for the first-build
6383
// scenario where no prebuilt cache exists yet.
@@ -116,7 +136,15 @@ function runFixes(pendingFixes, rootDir) {
116136
const [cmd, ...args] = fix;
117137
console.log(`$ ${fix.join(' ')}`);
118138
try {
119-
execFileSync(cmd, args, { stdio: 'inherit', cwd: rootDir });
139+
if (process.platform === 'win32') {
140+
execFileSync(
141+
process.env.ComSpec || 'cmd.exe',
142+
['/d', '/s', '/c', fix.join(' ')],
143+
{ stdio: 'inherit', cwd: rootDir },
144+
);
145+
} else {
146+
execFileSync(cmd, args, { stdio: 'inherit', cwd: rootDir });
147+
}
120148
} catch {
121149
console.error(`Fix command failed: ${fix.join(' ')}\n`);
122150
allSucceeded = false;

‎scripts/check-build-prereqs.test.mjs‎

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ const repoRoot = path.resolve(
1212
);
1313
const scriptPath = path.join(repoRoot, 'scripts/check-build-prereqs.mjs');
1414

15-
function createTestRoot({ nodeModules = false, mobileWebDist = false, sherpaOnnx = null } = {}) {
15+
function createTestRoot({
16+
nodeModules = false,
17+
mobileWebDist = false,
18+
pluginHostDist = false,
19+
sherpaOnnx = null,
20+
} = {}) {
1621
const root = mkdtempSync(path.join(tmpdir(), 'bitfun-build-prereqs-'));
1722

1823
if (nodeModules) {
@@ -25,6 +30,18 @@ function createTestRoot({ nodeModules = false, mobileWebDist = false, sherpaOnnx
2530
writeFileSync(path.join(distDir, 'index.html'), '<html></html>');
2631
}
2732

33+
if (pluginHostDist) {
34+
const distDir = path.join(
35+
root,
36+
'src',
37+
'apps',
38+
'extension-host',
39+
'dist',
40+
);
41+
mkdirSync(distDir, { recursive: true });
42+
writeFileSync(path.join(distDir, 'extension-host.js'), '');
43+
}
44+
2845
if (sherpaOnnx) {
2946
for (const version of sherpaOnnx) {
3047
const libDir = path.join(
@@ -44,21 +61,36 @@ function createTestRoot({ nodeModules = false, mobileWebDist = false, sherpaOnnx
4461

4562
function createFakePnpm() {
4663
const binDir = mkdtempSync(path.join(tmpdir(), 'bitfun-fake-pnpm-'));
47-
const pnpmPath = path.join(binDir, 'pnpm');
64+
const fakePnpmPath = path.join(binDir, 'fake-pnpm.cjs');
4865
writeFileSync(
49-
pnpmPath,
50-
`#!/usr/bin/env node
66+
fakePnpmPath,
67+
`
5168
const { mkdirSync, writeFileSync } = require('fs');
5269
const args = process.argv.slice(2);
5370
if (args[0] === 'install') {
5471
mkdirSync('node_modules', { recursive: true });
5572
} else if (args[0] === 'run' && args[1] === 'prepare:mobile-web') {
5673
mkdirSync('src/mobile-web/dist', { recursive: true });
5774
writeFileSync('src/mobile-web/dist/index.html', '<html></html>');
75+
} else if (args[0] === 'run' && args[1] === 'plugin-host:prepare') {
76+
mkdirSync('src/apps/extension-host/dist', { recursive: true });
77+
writeFileSync('src/apps/extension-host/dist/extension-host.js', '');
5878
}
5979
`,
6080
);
61-
chmodSync(pnpmPath, 0o755);
81+
if (process.platform === 'win32') {
82+
writeFileSync(
83+
path.join(binDir, 'pnpm.cmd'),
84+
`@echo off\r\n"${process.execPath}" "%~dp0fake-pnpm.cjs" %*\r\n`,
85+
);
86+
} else {
87+
const pnpmPath = path.join(binDir, 'pnpm');
88+
writeFileSync(
89+
pnpmPath,
90+
`#!/usr/bin/env node\nrequire('./fake-pnpm.cjs');\n`,
91+
);
92+
chmodSync(pnpmPath, 0o755);
93+
}
6294
return binDir;
6395
}
6496

@@ -90,20 +122,22 @@ test('passes when all prerequisites are present (including sherpa-onnx prebuilt)
90122
const root = createTestRoot({
91123
nodeModules: true,
92124
mobileWebDist: true,
125+
pluginHostDist: true,
93126
sherpaOnnx: ['sherpa-onnx-v1.13.4-osx-arm64-static-lib'],
94127
});
95128
t.after(() => rmSync(root, { recursive: true, force: true }));
96129

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

99-
assert.equal(result.status, 0);
132+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
100133
assert.match(result.stdout, /Build prerequisite check passed/);
101134
assert.doesNotMatch(result.stderr, /\[WARN\]/);
102135
});
103136

104137
test('fails when root node_modules is missing', (t) => {
105138
const root = createTestRoot({
106139
mobileWebDist: true,
140+
pluginHostDist: true,
107141
sherpaOnnx: ['sherpa-onnx-v1.13.4-osx-arm64-static-lib'],
108142
});
109143
t.after(() => rmSync(root, { recursive: true, force: true }));
@@ -118,6 +152,7 @@ test('fails when root node_modules is missing', (t) => {
118152
test('fails when mobile-web dist is missing', (t) => {
119153
const root = createTestRoot({
120154
nodeModules: true,
155+
pluginHostDist: true,
121156
sherpaOnnx: ['sherpa-onnx-v1.13.4-osx-arm64-static-lib'],
122157
});
123158
t.after(() => rmSync(root, { recursive: true, force: true }));
@@ -129,8 +164,27 @@ test('fails when mobile-web dist is missing', (t) => {
129164
assert.match(result.stderr, /Fix: pnpm run prepare:mobile-web/);
130165
});
131166

167+
test('fails when OpenCode extension Host dist is missing', (t) => {
168+
const root = createTestRoot({
169+
nodeModules: true,
170+
mobileWebDist: true,
171+
sherpaOnnx: ['sherpa-onnx-v1.13.4-osx-arm64-static-lib'],
172+
});
173+
t.after(() => rmSync(root, { recursive: true, force: true }));
174+
175+
const result = runCheck(root, { sherpaEnv: '' });
176+
177+
assert.notEqual(result.status, 0);
178+
assert.match(result.stderr, /\[FAIL\] OpenCode extension Host dist/);
179+
assert.match(result.stderr, /Fix: pnpm run plugin-host:prepare/);
180+
});
181+
132182
test('warns when sherpa-onnx prebuilt dir does not exist (first build)', (t) => {
133-
const root = createTestRoot({ nodeModules: true, mobileWebDist: true });
183+
const root = createTestRoot({
184+
nodeModules: true,
185+
mobileWebDist: true,
186+
pluginHostDist: true,
187+
});
134188
t.after(() => rmSync(root, { recursive: true, force: true }));
135189

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

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

164-
assert.equal(result.status, 0);
218+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
165219
assert.match(result.stdout, /Attempting fixes/);
166220
assert.match(result.stdout, /\$ pnpm install/);
167221
assert.match(result.stdout, /\$ pnpm run prepare:mobile-web/);

‎scripts/cli-product.mjs‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env node
2-
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
2+
import { copyFileSync, existsSync, mkdirSync, rmSync } from 'node:fs';
33
import { join, resolve } from 'node:path';
44
import { spawnSync } from 'node:child_process';
55
import { fileURLToPath } from 'node:url';
@@ -9,6 +9,24 @@ import { ensureProductOutputDirectory, productBuildEnvironment } from './product
99
import { ProductDefinitionError, resolveProductDefinition } from './product-customization/resolver.mjs';
1010

1111
const ROOT = resolve(import.meta.dirname, '..');
12+
const PLUGIN_HOST_DIST = join(ROOT, 'src', 'apps', 'extension-host', 'dist');
13+
const PLUGIN_HOST_ENTRIES = ['extension-host.js'];
14+
15+
export function stagePluginHostResources(destination, sourceDirectory = PLUGIN_HOST_DIST) {
16+
for (const entry of PLUGIN_HOST_ENTRIES) {
17+
const source = join(sourceDirectory, entry);
18+
if (!existsSync(source)) {
19+
throw new Error(
20+
`CLI plugin Host resource was not produced: ${source}. Run pnpm run plugin-host:prepare.`,
21+
);
22+
}
23+
}
24+
rmSync(destination, { recursive: true, force: true });
25+
mkdirSync(destination, { recursive: true });
26+
for (const entry of PLUGIN_HOST_ENTRIES) {
27+
copyFileSync(join(sourceDirectory, entry), join(destination, entry));
28+
}
29+
}
1230

1331
function stripDelimiter(args) {
1432
const result = [...args];
@@ -76,6 +94,12 @@ export function cliBuildPlan(resolution, mode, forwardArgs = [], platform = proc
7694
cargoArgs,
7795
internalBinaryPath: join(cargoTargetDir, ...(target ? [target] : []), profileDir, `bitfun${suffix}`),
7896
stagedBinaryPath: join(resolution.outputDir, 'package', `${resolution.assembly.binaryName}${suffix}`),
97+
stagedPluginHostPath: join(
98+
resolution.outputDir,
99+
'package',
100+
'resources',
101+
'ext-host',
102+
),
79103
};
80104
}
81105

@@ -93,7 +117,9 @@ function run(plan) {
93117
ensureProductOutputDirectory(plan.resolution);
94118
mkdirSync(join(plan.stagedBinaryPath, '..'), { recursive: true });
95119
copyFileSync(plan.internalBinaryPath, plan.stagedBinaryPath);
120+
stagePluginHostResources(plan.stagedPluginHostPath);
96121
console.log(`[product] staged CLI: ${plan.stagedBinaryPath}`);
122+
console.log(`[product] staged plugin Host: ${plan.stagedPluginHostPath}`);
97123
}
98124
}
99125

‎scripts/cli-product.test.mjs‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
import assert from 'node:assert/strict';
2+
import { mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
3+
import { tmpdir } from 'node:os';
24
import { join, resolve } from 'node:path';
35
import test from 'node:test';
46

5-
import { cliBuildPlan } from './cli-product.mjs';
7+
import { cliBuildPlan, stagePluginHostResources } from './cli-product.mjs';
68
import { resolveProductDefinition } from './product-customization/resolver.mjs';
79

810
const ROOT = resolve(import.meta.dirname, '..');
911
const ACME = join(ROOT, 'products', 'fixtures', 'acme', 'product.jsonc');
1012

13+
test('CLI stages only the supported plugin Host entry', (t) => {
14+
const root = mkdtempSync(join(tmpdir(), 'bitfun-cli-plugin-host-'));
15+
t.after(() => rmSync(root, { recursive: true, force: true }));
16+
const source = join(root, 'source');
17+
const destination = join(root, 'destination');
18+
mkdirSync(source);
19+
writeFileSync(join(source, 'extension-host.js'), 'current');
20+
writeFileSync(join(source, 'stale-runtime.js'), 'stale');
21+
22+
stagePluginHostResources(destination, source);
23+
24+
assert.deepEqual(readdirSync(destination), ['extension-host.js']);
25+
});
26+
1127
test('CLI uses the shared resolver and stages the internal binary under the member name', () => {
1228
const resolution = resolveProductDefinition({ rootDir: ROOT, productConfig: ACME, member: 'cli' });
1329
const plan = cliBuildPlan(resolution, 'build', ['--locked'], 'win32');
@@ -16,6 +32,7 @@ test('CLI uses the shared resolver and stages the internal binary under the memb
1632
assert.ok(plan.cargoArgs.includes('--locked'));
1733
assert.ok(plan.internalBinaryPath.endsWith('bitfun.exe'));
1834
assert.ok(plan.stagedBinaryPath.endsWith('acme.exe'));
35+
assert.ok(plan.stagedPluginHostPath.endsWith(join('resources', 'ext-host')));
1936
assert.equal(plan.environment.BITFUN_PRODUCT_DISPLAY_NAME, 'Acme CLI');
2037
});
2138

‎scripts/cli/package-contract.test.mjs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ for (const packageScript of [
1717
assert.match(content, /THIRD_PARTY_NOTICES\.md/);
1818
assert.match(content, /models-dev\.LICENSE\.txt/);
1919
assert.match(content, /models-dev\.provenance\.json/);
20+
assert.match(content, /extension-host/);
21+
assert.match(content, /resources[\\/]ext-host/);
22+
assert.match(content, /extension-host\.js/);
23+
}
24+
25+
for (const workflow of [
26+
'.github/workflows/cli-package.yml',
27+
'.github/workflows/cli-package-manual.yml',
28+
'.github/workflows/linux-binaries.yml',
29+
'.github/workflows/nightly.yml',
30+
]) {
31+
const content = read(workflow);
32+
assert.match(content, /oven-sh\/setup-bun@v2/);
33+
assert.match(content, /plugin Host resources/);
34+
assert.match(content, /plugin-host:prepare|extension-host/);
2035
}
2136

2237
for (const workflow of [

0 commit comments

Comments
 (0)