@opencode/plugin 0.0.0-dev-20713 → 0.0.0-dev-20716
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/runtime-modules.d.ts +6 -0
- package/dist/runtime-modules.js +101 -0
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.js +46 -0
- package/dist/source.bun.js +3 -0
- package/package.json +12 -8
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# `@opencode/plugin`
|
|
2
|
+
|
|
3
|
+
Authoring interfaces and runtime loader support for OpenCode V2 plugins:
|
|
4
|
+
|
|
5
|
+
- `@opencode/plugin` — [Promise plugin API](./src/README.md)
|
|
6
|
+
- `@opencode/plugin/effect` — [Effect plugin API](./src/effect/README.md)
|
|
7
|
+
- `@opencode/plugin/rpc` — portable RPC contract definitions
|
|
8
|
+
- `@opencode/plugin/tui` — terminal UI plugin API
|
|
9
|
+
|
|
10
|
+
## Packaging And Runtime `effect`
|
|
11
|
+
|
|
12
|
+
When the OpenCode CLI loads server or TUI plugins, it resolves imports of `effect`, exported `effect/*` subpaths, and `@opencode/plugin` entrypoints (including imports from dependencies inside a plugin's `node_modules`) to the host's runtime module instances so fibers, loggers, and `Schema` parsers share one copy.
|
|
13
|
+
|
|
14
|
+
- Declare `effect` as a `peerDependency` (and `devDependency` for local type-checking and testing) rather than a bundled runtime dependency.
|
|
15
|
+
- Do not bundle `effect` into published plugin files; if you build with a bundler, keep `effect` and `effect/*` external. Two copies of `effect` do not share fiber, logger, or `Schema` internals.
|
|
16
|
+
- Plugins and their `node_modules` dependencies always receive OpenCode's host `effect` instance. Use `effect` APIs and module paths compatible with the OpenCode release you target; dependencies built on another `effect` major (such as Effect 3) are not supported.
|
|
17
|
+
- Only public `effect` subpaths are provided. A plugin that imports one of Effect's private `internal` modules fails to load with an error naming the path.
|
|
18
|
+
- The compiled OpenCode binary does not include the Scalar and Swagger UI assets used by Effect's HTTP API docs pages; serving those pages from a plugin shows a notice instead.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type RuntimeModuleLoader = () => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
2
|
+
export declare function resolveHostPackageRoots(from?: string, packages?: readonly string[]): string[];
|
|
3
|
+
export declare function loadRuntimeModules(): Readonly<Record<string, RuntimeModuleLoader>>;
|
|
4
|
+
export declare function discoverPluginRuntimeSpecifiers(from?: string, packages?: readonly string[]): ReadonlyMap<string, string>;
|
|
5
|
+
export declare function pluginRuntimeLoaderCode(specifier: string, entries: ReadonlyMap<string, string>): string;
|
|
6
|
+
export declare function createLoader(resolved: string): RuntimeModuleLoader;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
const runtimePackages = ["effect", "@opencode/plugin"];
|
|
5
|
+
export function resolveHostPackageRoots(from = import.meta.dir, packages = runtimePackages) {
|
|
6
|
+
return packages.flatMap((pkgName) => {
|
|
7
|
+
const dir = path.dirname(Bun.resolveSync(`${pkgName}/package.json`, from));
|
|
8
|
+
return [dir, findNodeModulesDir(pkgName, from, dir)];
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export function loadRuntimeModules() {
|
|
12
|
+
const entries = discoverPluginRuntimeSpecifiers();
|
|
13
|
+
const effectEntry = entries.get("effect");
|
|
14
|
+
if (effectEntry)
|
|
15
|
+
require(effectEntry);
|
|
16
|
+
return Object.fromEntries([...entries.entries()].map(([specifier, resolved]) => [specifier, createLoader(resolved)]));
|
|
17
|
+
}
|
|
18
|
+
export function discoverPluginRuntimeSpecifiers(from = import.meta.dir, packages = runtimePackages) {
|
|
19
|
+
const entries = new Map();
|
|
20
|
+
for (const pkgName of packages) {
|
|
21
|
+
const realDir = path.dirname(Bun.resolveSync(`${pkgName}/package.json`, from));
|
|
22
|
+
// Resolve workspace @opencode/plugin paths via node_modules so OpenTUI does not wrap host files in async rewrite loaders.
|
|
23
|
+
const loadDir = findNodeModulesDir(pkgName, from, realDir);
|
|
24
|
+
const toLoadPath = (resolved) => loadDir === realDir ? resolved : path.join(loadDir, path.relative(realDir, resolved));
|
|
25
|
+
const rootEntry = Bun.resolveSync(pkgName, from);
|
|
26
|
+
const relParts = path.relative(realDir, rootEntry).replaceAll("\\", "/").split("/");
|
|
27
|
+
const scanDir = relParts.length > 1 ? path.join(realDir, relParts[0]) : realDir;
|
|
28
|
+
const ext = path.extname(rootEntry) || ".js";
|
|
29
|
+
entries.set(pkgName, toLoadPath(rootEntry));
|
|
30
|
+
for (const file of new Bun.Glob(`**/*${ext}`).scanSync({ cwd: scanDir })) {
|
|
31
|
+
const normalized = file.replaceAll("\\", "/");
|
|
32
|
+
if (normalized.startsWith("internal/") ||
|
|
33
|
+
normalized.includes("/internal/") ||
|
|
34
|
+
normalized.startsWith("source.") ||
|
|
35
|
+
normalized.startsWith("runtime")) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const base = normalized.slice(0, -ext.length);
|
|
39
|
+
if (base === "index")
|
|
40
|
+
continue;
|
|
41
|
+
const candidates = base.endsWith("/index")
|
|
42
|
+
? [`${pkgName}/${base.slice(0, -"/index".length)}`, `${pkgName}/${base}`]
|
|
43
|
+
: [`${pkgName}/${base}`];
|
|
44
|
+
for (const specifier of candidates) {
|
|
45
|
+
if (entries.has(specifier))
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
entries.set(specifier, toLoadPath(Bun.resolveSync(specifier, from)));
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
56
|
+
export function pluginRuntimeLoaderCode(specifier, entries) {
|
|
57
|
+
if (specifier.startsWith("effect/")) {
|
|
58
|
+
const slash = specifier.lastIndexOf("/");
|
|
59
|
+
const parent = specifier.slice(0, slash);
|
|
60
|
+
const member = specifier.slice(slash + 1);
|
|
61
|
+
const parentResolved = entries.get(parent);
|
|
62
|
+
const resolved = entries.get(specifier);
|
|
63
|
+
if (member !== "index" &&
|
|
64
|
+
parentResolved &&
|
|
65
|
+
resolved &&
|
|
66
|
+
require(parentResolved)[member] === require(resolved)) {
|
|
67
|
+
return `() => require(${JSON.stringify(parent)})[${JSON.stringify(member)}]`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return `() => require(${JSON.stringify(specifier)})`;
|
|
71
|
+
}
|
|
72
|
+
export function createLoader(resolved) {
|
|
73
|
+
let cached;
|
|
74
|
+
let pending;
|
|
75
|
+
return () => {
|
|
76
|
+
if (cached)
|
|
77
|
+
return cached;
|
|
78
|
+
if (pending)
|
|
79
|
+
return pending;
|
|
80
|
+
try {
|
|
81
|
+
return (cached = require(resolved));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return (pending = import(pathToFileURL(resolved).href).then((mod) => (cached = mod), (error) => {
|
|
85
|
+
pending = undefined;
|
|
86
|
+
throw error;
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function findNodeModulesDir(pkgName, from, realDir) {
|
|
92
|
+
if (/[/\\]node_modules[/\\]/.test(realDir))
|
|
93
|
+
return realDir;
|
|
94
|
+
for (let dir = path.resolve(from);; dir = path.dirname(dir)) {
|
|
95
|
+
const candidate = path.join(dir, "node_modules", pkgName);
|
|
96
|
+
if (existsSync(candidate) && realpathSync(candidate) === realDir)
|
|
97
|
+
return candidate;
|
|
98
|
+
if (path.dirname(dir) === dir)
|
|
99
|
+
return realDir;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type RuntimeModuleLoader } from "./runtime-modules.js";
|
|
2
|
+
export declare function provides(specifier: string): boolean;
|
|
3
|
+
export declare function ensurePluginRuntime(): Readonly<Record<string, RuntimeModuleLoader>>;
|
|
4
|
+
export declare function createForeignPackageFilter(rootsInput?: Iterable<string>): RegExp;
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { loadRuntimeModules, resolveHostPackageRoots } from "./runtime-modules.js";
|
|
2
|
+
const foreignPkgSuffix = String.raw `(?:node_modules[/\\](@opencode[/\\]plugin|effect)|(@opencode[/\\]plugin|effect)@[^/\\]+@@@\d+)`;
|
|
3
|
+
const foreignPkgPattern = new RegExp(String.raw `^(.*[/\\]${foreignPkgSuffix})[/\\](.+)$`);
|
|
4
|
+
let installed;
|
|
5
|
+
export function provides(specifier) {
|
|
6
|
+
return installed !== undefined && Object.hasOwn(installed, specifier);
|
|
7
|
+
}
|
|
8
|
+
export function ensurePluginRuntime() {
|
|
9
|
+
if (typeof Bun === "undefined")
|
|
10
|
+
return {};
|
|
11
|
+
if (installed)
|
|
12
|
+
return installed;
|
|
13
|
+
const modules = loadRuntimeModules();
|
|
14
|
+
installed = modules;
|
|
15
|
+
Bun.plugin({
|
|
16
|
+
name: "opencode-plugin-runtime",
|
|
17
|
+
setup(build) {
|
|
18
|
+
for (const [specifier, load] of Object.entries(modules)) {
|
|
19
|
+
build.module(specifier, () => {
|
|
20
|
+
const exports = load();
|
|
21
|
+
return exports instanceof Promise
|
|
22
|
+
? exports.then((value) => ({ exports: value, loader: "object" }))
|
|
23
|
+
: { exports, loader: "object" };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
build.onLoad({ filter: createForeignPackageFilter() }, (args) => {
|
|
27
|
+
const match = args.path.match(foreignPkgPattern);
|
|
28
|
+
const target = match ? `${match[2] ?? match[3]}/${match[4]}`.replaceAll("\\", "/") : args.path;
|
|
29
|
+
throw new Error(`Cannot load "${target}" from plugin node_modules: "${target}" is not provided by OpenCode; plugins must use the host's "effect" and "@opencode/plugin" modules.`);
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
return modules;
|
|
34
|
+
}
|
|
35
|
+
export function createForeignPackageFilter(rootsInput = resolveHostPackageRoots()) {
|
|
36
|
+
const suffix = String.raw `[/\\]${foreignPkgSuffix}[/\\].*\.[cm]?[jt]sx?(?:[?#].*)?$`;
|
|
37
|
+
const roots = [...new Set(rootsInput)];
|
|
38
|
+
if (roots.length === 0)
|
|
39
|
+
return new RegExp(suffix);
|
|
40
|
+
const escaped = roots
|
|
41
|
+
.map((value) => value
|
|
42
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
43
|
+
.replace(/(?:\\\/|\\\\|\/)+/g, "[/\\\\]"))
|
|
44
|
+
.join("|");
|
|
45
|
+
return new RegExp(`^(?!(?:${escaped})[/\\\\]).*${suffix}`);
|
|
46
|
+
}
|
package/dist/source.bun.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { Host } from "./host.js";
|
|
6
|
+
import { provides } from "./runtime.js";
|
|
6
7
|
import { localSource } from "./source.js";
|
|
7
8
|
import { missingPackageTarget } from "./source.package.js";
|
|
8
9
|
let generation = Date.now();
|
|
@@ -41,6 +42,8 @@ export async function prepareSource(entrypoint, track) {
|
|
|
41
42
|
? new URL(item.path, pathToFileURL(file))
|
|
42
43
|
: localSource(item.path, path.dirname(file));
|
|
43
44
|
if (!local) {
|
|
45
|
+
if (provides(item.path))
|
|
46
|
+
continue;
|
|
44
47
|
try {
|
|
45
48
|
Bun.resolveSync(item.path, path.dirname(file));
|
|
46
49
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@opencode/plugin",
|
|
4
|
-
"version": "0.0.0-dev-
|
|
4
|
+
"version": "0.0.0-dev-20716",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"scripts": {
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"import": "./dist/host.js",
|
|
23
23
|
"types": "./dist/host.d.ts"
|
|
24
24
|
},
|
|
25
|
+
"./runtime": {
|
|
26
|
+
"import": "./dist/runtime.js",
|
|
27
|
+
"types": "./dist/runtime.d.ts"
|
|
28
|
+
},
|
|
25
29
|
"./tui": {
|
|
26
30
|
"import": "./dist/tui/index.js",
|
|
27
31
|
"types": "./dist/tui/index.d.ts"
|
|
@@ -43,15 +47,15 @@
|
|
|
43
47
|
],
|
|
44
48
|
"dependencies": {
|
|
45
49
|
"@ai-sdk/provider": "3.0.8",
|
|
46
|
-
"@opencode/ai": "0.0.0-dev-
|
|
47
|
-
"@opencode/client": "0.0.0-dev-
|
|
48
|
-
"@opencode/protocol": "0.0.0-dev-
|
|
49
|
-
"@opencode/schema": "0.0.0-dev-
|
|
50
|
-
"@opencode/util": "0.0.0-dev-
|
|
50
|
+
"@opencode/ai": "0.0.0-dev-20716",
|
|
51
|
+
"@opencode/client": "0.0.0-dev-20716",
|
|
52
|
+
"@opencode/protocol": "0.0.0-dev-20716",
|
|
53
|
+
"@opencode/schema": "0.0.0-dev-20716",
|
|
54
|
+
"@opencode/util": "0.0.0-dev-20716",
|
|
51
55
|
"effect": "4.0.0-rc.112"
|
|
52
56
|
},
|
|
53
57
|
"peerDependencies": {
|
|
54
|
-
"@opencode/theme": "0.0.0-dev-
|
|
58
|
+
"@opencode/theme": "0.0.0-dev-20716",
|
|
55
59
|
"@opentui/core": ">=0.5.16",
|
|
56
60
|
"@opentui/solid": ">=0.5.16",
|
|
57
61
|
"solid-js": ">=1.9.0"
|
|
@@ -71,7 +75,7 @@
|
|
|
71
75
|
}
|
|
72
76
|
},
|
|
73
77
|
"devDependencies": {
|
|
74
|
-
"@opencode/theme": "0.0.0-dev-
|
|
78
|
+
"@opencode/theme": "0.0.0-dev-20716",
|
|
75
79
|
"@opentui/core": "0.5.16",
|
|
76
80
|
"@opentui/solid": "0.5.16",
|
|
77
81
|
"@tsconfig/bun": "1.0.9",
|