mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 06:54:06 +00:00
92 lines
3.4 KiB
TypeScript
92 lines
3.4 KiB
TypeScript
export * as Host from "./host.js"
|
|
|
|
import { existsSync, readFileSync } from "node:fs"
|
|
import path from "node:path"
|
|
import { Option, Schema } from "effect"
|
|
import { importModule, resolveModule } from "@opencode/util/runtime-import"
|
|
|
|
export interface Target {
|
|
readonly directory: string
|
|
readonly name?: string
|
|
}
|
|
|
|
export interface Entrypoints {
|
|
readonly server?: string
|
|
readonly tui?: string
|
|
readonly rpc?: string
|
|
/**
|
|
* The prebuilt CommonJS GUI bundle (`./gui-experimental`, or `gui-experimental.cjs` or `gui-experimental.js` in a
|
|
* local directory without `exports`). Each desktop app window runs its default export; the desktop app's main
|
|
* process runs its `main` export.
|
|
*/
|
|
readonly gui?: string
|
|
}
|
|
|
|
export function resolve(input: Target): Entrypoints {
|
|
// A local directory whose package.json names the package and maps `exports` resolves through them, like the
|
|
// published package; without them it uses the conventional file names at its root.
|
|
const manifest = input.name ? undefined : packageManifest(input.directory)
|
|
const target =
|
|
typeof manifest?.name === "string" && manifest.exports !== undefined ? { ...input, name: manifest.name } : input
|
|
const entry = (subpaths: readonly string[]) => {
|
|
for (const subpath of subpaths) {
|
|
const specifier = target.name
|
|
? [target.name, subpath].filter(Boolean).join("/")
|
|
: path.resolve(target.directory, subpath || "index")
|
|
try {
|
|
return resolveModule(specifier, target.directory)
|
|
} catch (error) {
|
|
if (
|
|
!(error instanceof Error) ||
|
|
!("code" in error) ||
|
|
![
|
|
"ENOENT",
|
|
"ENOTDIR",
|
|
"MODULE_NOT_FOUND",
|
|
"ERR_MODULE_NOT_FOUND",
|
|
"ERR_PACKAGE_PATH_NOT_EXPORTED",
|
|
"ERR_UNSUPPORTED_DIR_IMPORT",
|
|
].includes(String(error.code))
|
|
)
|
|
throw error
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
return {
|
|
server: entry(["server", ""]),
|
|
tui: entry(["tui"]),
|
|
rpc: entry(["rpc"]),
|
|
// The app evaluates the bundle as CommonJS text, so a TypeScript source or an ES module is never a GUI entry.
|
|
gui: [target.name ? entry(["gui-experimental"]) : entry(["gui-experimental.cjs", "gui-experimental.js"])].find(
|
|
(file) => file && /\.c?js$/.test(file),
|
|
),
|
|
}
|
|
}
|
|
|
|
export function load(entrypoint: string): Promise<unknown> {
|
|
return importModule(entrypoint)
|
|
}
|
|
|
|
/**
|
|
* The id of a plugin without a server part: its package name, or a local directory's name when it has no
|
|
* package.json name. A server part names its plugin in `Plugin.define` instead.
|
|
*/
|
|
export function packageID(target: Target) {
|
|
if (target.name) return target.name
|
|
const name = packageManifest(target.directory)?.name
|
|
return typeof name === "string" && name ? name : path.basename(target.directory)
|
|
}
|
|
|
|
// The fields resolution reads from a local directory's package.json.
|
|
const Manifest = Schema.fromJsonString(
|
|
Schema.Struct({ name: Schema.optional(Schema.Unknown), exports: Schema.optional(Schema.Unknown) }),
|
|
)
|
|
|
|
// A package.json that is not a JSON object counts as missing, so one broken plugin directory never stops the rest
|
|
// from resolving: it falls back to the conventional root file names.
|
|
function packageManifest(directory: string) {
|
|
const file = path.join(directory, "package.json")
|
|
if (!existsSync(file)) return undefined
|
|
return Option.getOrUndefined(Schema.decodeUnknownOption(Manifest)(readFileSync(file, "utf8")))
|
|
}
|