Merge pull request #3167 from affaan-m/fix/opencode-plugin-entry

fix(opencode): add resolvable package entry and loadable in-place sources
This commit is contained in:
Affaan Mustafa
2026-09-18 20:45:06 -04:00
committed by GitHub
8 changed files with 98 additions and 19 deletions
+1 -1
View File
@@ -37,4 +37,4 @@
// Export the main plugin
// opencode's legacy plugin loader iterates every module export and throws if
// any is not a plugin function, so only the plugin function may be exported.
export { default } from "./plugins/index.js"
export { default } from "./plugins/index.ts"
+4 -4
View File
@@ -16,8 +16,8 @@
import type { PluginInput } from "@opencode-ai/plugin"
import * as fs from "fs"
import * as path from "path"
import changedFilesTool from "../tools/changed-files.js"
import dependencyAnalyzerTool from "../tools/dependency-analyzer.js"
import changedFilesTool from "../tools/changed-files.ts"
import dependencyAnalyzerTool from "../tools/dependency-analyzer.ts"
/**
* Type definitions for better type safety
@@ -111,9 +111,9 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
// This plugin is OpenCode's startup entry point, so a static import
// failure here previously crashed the whole plugin -- and with it, the
// entire OpenCode session -- before any hooks could load (see #2530).
let changedFilesStore: typeof import("./lib/changed-files-store.js") | undefined
let changedFilesStore: typeof import("./lib/changed-files-store.ts") | undefined
try {
const store = await import("./lib/changed-files-store.js")
const store = await import("./lib/changed-files-store.ts")
store.initStore(worktreePath)
changedFilesStore = store
} catch {
+2 -2
View File
@@ -6,7 +6,7 @@
* while taking advantage of OpenCode's more sophisticated 20+ event types.
*/
export { ECCHooksPlugin, default } from "./ecc-hooks.js"
export { ECCHooksPlugin, default } from "./ecc-hooks.ts"
// Re-export for named imports
export * from "./ecc-hooks.js"
export * from "./ecc-hooks.ts"
+3 -3
View File
@@ -1,5 +1,5 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
import type { ChangeType, TreeNode } from "../plugins/lib/changed-files-store.js"
import type { ChangeType, TreeNode } from "../plugins/lib/changed-files-store.ts"
const INDICATORS: Record<ChangeType, string> = {
added: "+",
@@ -27,12 +27,12 @@ function renderTree(nodes: TreeNode[], indent: string): string {
// file, so a static import failure here previously took down the entire
// tools module -- and with it, the whole OpenCode session -- on the very
// first tool-loading pass (see #2530).
type ChangedFilesStore = typeof import("../plugins/lib/changed-files-store.js")
type ChangedFilesStore = typeof import("../plugins/lib/changed-files-store.ts")
let changedFilesStorePromise: Promise<ChangedFilesStore> | undefined
async function loadChangedFilesStore(): Promise<ChangedFilesStore> {
if (!changedFilesStorePromise) {
changedFilesStorePromise = import("../plugins/lib/changed-files-store.js").catch(() => {
changedFilesStorePromise = import("../plugins/lib/changed-files-store.ts").catch(() => {
changedFilesStorePromise = undefined
throw new Error(
"changed-files tool: could not load the changed-files store. " +
+8 -8
View File
@@ -5,11 +5,11 @@
*/
// Re-export all tools
export { default as runTests } from "./run-tests.js"
export { default as checkCoverage } from "./check-coverage.js"
export { default as securityAudit } from "./security-audit.js"
export { default as formatCode } from "./format-code.js"
export { default as lintCheck } from "./lint-check.js"
export { default as gitSummary } from "./git-summary.js"
export { default as changedFiles } from "./changed-files.js"
export { default as dependencyAnalyzer } from "./dependency-analyzer.js"
export { default as runTests } from "./run-tests.ts"
export { default as checkCoverage } from "./check-coverage.ts"
export { default as securityAudit } from "./security-audit.ts"
export { default as formatCode } from "./format-code.ts"
export { default as lintCheck } from "./lint-check.ts"
export { default as gitSummary } from "./git-summary.ts"
export { default as changedFiles } from "./changed-files.ts"
export { default as dependencyAnalyzer } from "./dependency-analyzer.ts"
+2 -1
View File
@@ -15,7 +15,8 @@
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"types": ["node"]
},
"include": [
+11
View File
@@ -2,6 +2,17 @@
"name": "ecc-universal",
"version": "2.2.1",
"description": "Harness-native agent operating system for Codex, OpenCode, Cursor, Gemini, Claude Code, and terminal workflows - skills, hooks, rules, MCP conventions, and operator control-plane patterns",
"main": ".opencode/dist/index.js",
"types": ".opencode/dist/index.d.ts",
"exports": {
".": {
"types": "./.opencode/dist/index.d.ts",
"import": "./.opencode/dist/index.js",
"default": "./.opencode/dist/index.js"
},
"./package.json": "./package.json",
"./*": "./*"
},
"publishConfig": {
"access": "public"
},
+67
View File
@@ -4,6 +4,7 @@
const assert = require("assert")
const fs = require("fs")
const os = require("os")
const path = require("path")
const { spawnSync } = require("child_process")
const { getNpmPackEntry } = require("../lib/npm-pack-output")
@@ -46,6 +47,72 @@ function main() {
assert.strictEqual(result.status, 0, result.stderr)
assert.ok(fs.existsSync(distEntry), ".opencode/dist/index.js should exist after build")
}],
["package.json declares a resolvable OpenCode plugin entry", () => {
assert.strictEqual(packageJson.main, ".opencode/dist/index.js")
assert.ok(packageJson.exports, "package.json must declare an exports map")
assert.deepStrictEqual(packageJson.exports["."], {
types: "./.opencode/dist/index.d.ts",
import: "./.opencode/dist/index.js",
default: "./.opencode/dist/index.js",
})
}],
["installed package resolves and imports its root module by name", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-opencode-entry-"))
try {
fs.mkdirSync(path.join(tempDir, "node_modules"), { recursive: true })
fs.symlinkSync(
repoRoot,
path.join(tempDir, "node_modules", "ecc-universal"),
process.platform === "win32" ? "junction" : "dir"
)
const probe = `
const resolved = import.meta.resolve("ecc-universal")
if (!resolved.endsWith("/.opencode/dist/index.js")) {
throw new Error("unexpected entry resolution: " + resolved)
}
const mod = await import("ecc-universal")
if (Object.keys(mod).join(",") !== "default" || typeof mod.default !== "function") {
throw new Error("root module must export exactly the plugin function")
}
`
const probePath = path.join(tempDir, "probe.mjs")
fs.writeFileSync(probePath, probe)
const result = spawnSync(process.execPath, [probePath], {
cwd: tempDir,
encoding: "utf8",
})
assert.strictEqual(result.status, 0, result.stderr)
} finally {
fs.rmSync(tempDir, { recursive: true, force: true })
}
}],
["OpenCode TypeScript sources resolve their relative imports in place", () => {
const opencodeDir = path.join(repoRoot, ".opencode")
const sourceFiles = []
const walk = (dir) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const entryPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
if (entry.name !== "node_modules" && entry.name !== "dist") walk(entryPath)
} else if (entry.name.endsWith(".ts")) {
sourceFiles.push(entryPath)
}
}
}
walk(opencodeDir)
assert.ok(sourceFiles.length > 0, "expected OpenCode TypeScript sources")
const unresolved = []
for (const sourceFile of sourceFiles) {
const source = fs.readFileSync(sourceFile, "utf8")
for (const match of source.matchAll(/(?:from|import)\s*\(?\s*"(\.[^"]+)"/g)) {
const target = path.resolve(path.dirname(sourceFile), match[1])
if (!fs.existsSync(target)) {
unresolved.push(`${path.relative(repoRoot, sourceFile)} -> ${match[1]}`)
}
}
}
assert.deepStrictEqual(unresolved, [])
}],
["built OpenCode entry exports only the plugin function", () => {
const check = `
const assert = require("assert")