mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 20:45:11 +02:00
fix(opencode): add resolvable package entry and loadable in-place sources
Root package.json declared no main or exports, so OpenCode npm plugin resolution (import.meta.resolve) failed and the plugin was silently skipped (#3127). The .opencode TypeScript sources imported siblings with .js specifiers that only exist after compilation, so the home install, which loads the .ts files in place, crashed the tool registry with ERR_MODULE_NOT_FOUND (#3112). Declare main/types/exports on the root package pointing at the compiled plugin entry, switch the sources to .ts specifiers, and enable allowImportingTsExtensions with rewriteRelativeImportExtensions so the emitted dist keeps working .js specifiers. Add smoke tests that build the package, resolve and import the entry by name from a temp install, and verify every in-place relative import resolves. Fixes #3127 Fixes #3112
This commit is contained in:
+1
-1
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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. " +
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user