diff --git a/.opencode/index.ts b/.opencode/index.ts index fa6cadc58..8ee800f80 100644 --- a/.opencode/index.ts +++ b/.opencode/index.ts @@ -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" diff --git a/.opencode/plugins/ecc-hooks.ts b/.opencode/plugins/ecc-hooks.ts index 22b1132f0..69b59727e 100644 --- a/.opencode/plugins/ecc-hooks.ts +++ b/.opencode/plugins/ecc-hooks.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 { diff --git a/.opencode/plugins/index.ts b/.opencode/plugins/index.ts index c1e17a159..3a98f0ba6 100644 --- a/.opencode/plugins/index.ts +++ b/.opencode/plugins/index.ts @@ -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" diff --git a/.opencode/tools/changed-files.ts b/.opencode/tools/changed-files.ts index 1150ca756..3ae000e1b 100644 --- a/.opencode/tools/changed-files.ts +++ b/.opencode/tools/changed-files.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 = { 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 | undefined async function loadChangedFilesStore(): Promise { 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. " + diff --git a/.opencode/tools/index.ts b/.opencode/tools/index.ts index 9bd999479..17db1081a 100644 --- a/.opencode/tools/index.ts +++ b/.opencode/tools/index.ts @@ -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" diff --git a/.opencode/tsconfig.json b/.opencode/tsconfig.json index c6b43257b..1d586042f 100644 --- a/.opencode/tsconfig.json +++ b/.opencode/tsconfig.json @@ -15,7 +15,8 @@ "sourceMap": true, "resolveJsonModule": true, "isolatedModules": true, - "verbatimModuleSyntax": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, "types": ["node"] }, "include": [ diff --git a/package.json b/package.json index 839f94081..70a31ff8d 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/tests/scripts/build-opencode.test.js b/tests/scripts/build-opencode.test.js index 469165883..b8743b3e0 100644 --- a/tests/scripts/build-opencode.test.js +++ b/tests/scripts/build-opencode.test.js @@ -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")