mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 12:15:29 +02:00
fix: resolve OpenCode source imports and installed skills paths
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"
|
||||
|
||||
Generated
+1
-1
@@ -11,7 +11,7 @@
|
||||
"devDependencies": {
|
||||
"@opencode-ai/plugin": "^1.4.3",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"devDependencies": {
|
||||
"@opencode-ai/plugin": "^1.4.3",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -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. " +
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
* These tools extend OpenCode with additional capabilities.
|
||||
*/
|
||||
|
||||
// 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"
|
||||
// OpenCode loads these sources in place; tsc rewrites .ts imports for dist.
|
||||
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"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
|
||||
@@ -32,7 +32,7 @@ const {
|
||||
updateSettingsAtomic,
|
||||
validateManagedHooks,
|
||||
} = require('./install/claude-settings');
|
||||
const { adaptAntigravityAgent } = require('./install/antigravity-agent');
|
||||
const { transformInstallContent } = require('./install/content-transform');
|
||||
const { buildInstallIndex, rewriteRelativeLinks } = require('./install/link-rewrite');
|
||||
const { getInstallTargetAdapter, listInstallTargetAdapters } = require('./install-targets/registry');
|
||||
const { resolveInvocationEnvironment } = require('./invocation-environment');
|
||||
@@ -223,18 +223,8 @@ function buildLinkIndexForOperations(operations, trustedRoot) {
|
||||
return buildInstallIndex(mappings);
|
||||
}
|
||||
|
||||
function transformCopyFileContent(operation, content) {
|
||||
if (!operation.contentTransform) {
|
||||
return content;
|
||||
}
|
||||
if (operation.contentTransform === 'antigravity-agent-frontmatter') {
|
||||
return adaptAntigravityAgent(content, operation.sourceRelativePath);
|
||||
}
|
||||
throw new Error(`Unknown install content transform: ${operation.contentTransform}`);
|
||||
}
|
||||
|
||||
function getExpectedCopyFileContent(operation, content, linkIndex) {
|
||||
const transformed = transformCopyFileContent(operation, content);
|
||||
const transformed = transformInstallContent(operation, content);
|
||||
if (!linkIndex || !operation.sourceRelativePath || !isMarkdownPath(operation.destinationPath)) {
|
||||
return transformed;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ const path = require('path');
|
||||
const {
|
||||
buildValidationIssue,
|
||||
createInstallTargetAdapter,
|
||||
createManagedOperation,
|
||||
isForeignPlatformPath,
|
||||
} = require('./helpers');
|
||||
const { resolveOpencodeConfigRoot } = require('../opencode-paths');
|
||||
|
||||
@@ -88,5 +90,27 @@ module.exports = createInstallTargetAdapter({
|
||||
resolveRoot: resolveOpencodeConfigRoot,
|
||||
installStatePathSegments: ['ecc-install-state.json'],
|
||||
nativeRootRelativePath: '.opencode',
|
||||
planOperations(input, adapter) {
|
||||
const modules = Array.isArray(input.modules)
|
||||
? input.modules
|
||||
: (input.module ? [input.module] : []);
|
||||
return modules.flatMap(module => (Array.isArray(module.paths) ? module.paths : [])
|
||||
.filter(sourcePath => !isForeignPlatformPath(sourcePath, adapter.target))
|
||||
.flatMap(sourcePath => {
|
||||
const operation = adapter.createScaffoldOperation(module.id, sourcePath, input);
|
||||
if (operation.sourceRelativePath !== '.opencode') {
|
||||
return [operation];
|
||||
}
|
||||
// Keep the repository template's sibling skills path for direct use,
|
||||
// but point home installs at the skills copied inside the config root.
|
||||
return [operation, createManagedOperation({
|
||||
moduleId: module.id,
|
||||
sourceRelativePath: '.opencode/opencode.json',
|
||||
destinationPath: path.join(operation.destinationPath, 'opencode.json'),
|
||||
strategy: operation.strategy,
|
||||
contentTransform: 'opencode-home-skills-path',
|
||||
})];
|
||||
}));
|
||||
},
|
||||
validate: defaultValidateOpencodeHome,
|
||||
});
|
||||
|
||||
@@ -35,22 +35,12 @@ const {
|
||||
} = require('./ownership-guard');
|
||||
const { cleanupLegacyOpencodeInstall } = require('./opencode-legacy-migration');
|
||||
const { buildInstallIndex, rewriteRelativeLinks } = require('./link-rewrite');
|
||||
const { adaptAntigravityAgent } = require('./antigravity-agent');
|
||||
const { transformInstallContent } = require('./content-transform');
|
||||
|
||||
function isMarkdownPath(filePath) {
|
||||
return /\.(md|mdx|markdown)$/i.test(String(filePath || ''));
|
||||
}
|
||||
|
||||
function transformInstallContent(operation, content) {
|
||||
if (!operation.contentTransform) {
|
||||
return content;
|
||||
}
|
||||
if (operation.contentTransform === 'antigravity-agent-frontmatter') {
|
||||
return adaptAntigravityAgent(content, operation.sourceRelativePath);
|
||||
}
|
||||
throw new Error(`Unknown install content transform: ${operation.contentTransform}`);
|
||||
}
|
||||
|
||||
// Map every copy-file operation to { sourceRel, destRel } so relative links in
|
||||
// namespaced markdown can be rewritten to the file's actual installed location
|
||||
// (issue #2340). Returns null when the plan lacks the data needed to do so.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
const { adaptAntigravityAgent } = require('./antigravity-agent');
|
||||
|
||||
function transformInstallContent(operation, content) {
|
||||
if (!operation.contentTransform) {
|
||||
return content;
|
||||
}
|
||||
if (operation.contentTransform === 'antigravity-agent-frontmatter') {
|
||||
return adaptAntigravityAgent(content, operation.sourceRelativePath);
|
||||
}
|
||||
if (operation.contentTransform === 'opencode-home-skills-path') {
|
||||
const config = JSON.parse(content);
|
||||
if (!Array.isArray(config?.skills?.paths) || !config.skills.paths.includes('../skills')) {
|
||||
return content;
|
||||
}
|
||||
const installedConfig = {
|
||||
...config,
|
||||
skills: {
|
||||
...config.skills,
|
||||
paths: config.skills.paths.map(skillPath => skillPath === '../skills' ? './skills' : skillPath),
|
||||
},
|
||||
};
|
||||
return `${JSON.stringify(installedConfig, null, 2)}\n`;
|
||||
}
|
||||
throw new Error(`Unknown install content transform: ${operation.contentTransform}`);
|
||||
}
|
||||
|
||||
module.exports = { transformInstallContent };
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { readFile } from "node:fs/promises"
|
||||
import ts from "typescript"
|
||||
|
||||
// Emulate OpenCode's in-place TypeScript loading without remapping missing
|
||||
// .js specifiers to .ts files. Such remapping would hide issue #3112.
|
||||
export async function load(url, context, nextLoad) {
|
||||
if (!url.endsWith(".ts")) return nextLoad(url, context)
|
||||
|
||||
const source = await readFile(new URL(url), "utf8")
|
||||
const result = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
verbatimModuleSyntax: true,
|
||||
},
|
||||
})
|
||||
return { format: "module", source: result.outputText, shortCircuit: true }
|
||||
}
|
||||
@@ -21,6 +21,7 @@ const {
|
||||
} = require('../../scripts/lib/install-executor');
|
||||
const { applyInstallPlan: applyInstallPlanDirect } = require('../../scripts/lib/install/apply');
|
||||
const { withHookConsent } = require('../../scripts/lib/install/hook-consent');
|
||||
const { repairInstalledStates, uninstallInstalledStates } = require('../../scripts/lib/install-lifecycle');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
||||
|
||||
@@ -709,6 +710,82 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('OpenCode home installs resolve bundled skills inside the config root', () => {
|
||||
const tempDir = createTempDir('install-executor-opencode-skills-');
|
||||
try {
|
||||
const sourceRoot = path.join(tempDir, 'source');
|
||||
writeManifestSourceFixture(sourceRoot);
|
||||
const modulesPath = path.join(sourceRoot, 'manifests', 'install-modules.json');
|
||||
const manifest = JSON.parse(fs.readFileSync(modulesPath, 'utf8'));
|
||||
writeJson(sourceRoot, 'manifests/install-modules.json', {
|
||||
...manifest,
|
||||
modules: manifest.modules.map(module => ({
|
||||
...module,
|
||||
paths: ['.opencode', '.cursor', 'skills/demo'],
|
||||
targets: ['opencode'],
|
||||
})),
|
||||
});
|
||||
writeFile(sourceRoot, '.opencode/dist/index.js', 'export default () => ({});\n');
|
||||
writeFile(sourceRoot, '.opencode/dist/plugins/index.js', 'export {};\n');
|
||||
writeFile(sourceRoot, '.opencode/dist/tools/index.js', 'export {};\n');
|
||||
writeFile(sourceRoot, '.cursor/foreign.txt', 'must not install\n');
|
||||
const sourceConfig = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, '.opencode', 'opencode.json'), 'utf8'));
|
||||
const configs = [
|
||||
{
|
||||
...sourceConfig,
|
||||
skills: { ...sourceConfig.skills, paths: ['../skills', '/custom/skills', '../other-skills'] },
|
||||
},
|
||||
{ ...sourceConfig, skills: { paths: ['/custom/skills'] } },
|
||||
{ plugin: ['./plugins'] },
|
||||
];
|
||||
|
||||
for (const [index, config] of configs.entries()) {
|
||||
const configSourcePath = path.join(sourceRoot, '.opencode', 'opencode.json');
|
||||
writeJson(sourceRoot, '.opencode/opencode.json', config);
|
||||
const originalContent = fs.readFileSync(configSourcePath, 'utf8');
|
||||
const plan = createManifestInstallPlan({
|
||||
sourceRoot,
|
||||
projectRoot: path.join(tempDir, 'project'),
|
||||
homeDir: path.join(tempDir, `home-${index}`),
|
||||
target: 'opencode',
|
||||
moduleIds: ['fixture-core'],
|
||||
});
|
||||
const configPath = path.join(plan.targetRoot, 'opencode.json');
|
||||
assert.strictEqual(plan.operations.filter(operation => operation.destinationPath === configPath).length, 1);
|
||||
applyInstallPlan(plan);
|
||||
const installed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
const expected = index === 0
|
||||
? { ...config, skills: { ...config.skills, paths: ['./skills', '/custom/skills', '../other-skills'] } }
|
||||
: config;
|
||||
assert.deepStrictEqual(installed, expected);
|
||||
assert.strictEqual(fs.readFileSync(configSourcePath, 'utf8'), originalContent, 'repository template must stay unchanged');
|
||||
assert.ok(fs.existsSync(path.join(plan.targetRoot, 'skills', 'demo', 'SKILL.md')));
|
||||
assert.ok(!fs.existsSync(path.join(plan.targetRoot, '.cursor')));
|
||||
if (index === 0) {
|
||||
const lifecycleOptions = {
|
||||
repoRoot: sourceRoot,
|
||||
projectRoot: path.join(tempDir, 'project'),
|
||||
homeDir: path.join(tempDir, `home-${index}`),
|
||||
targets: ['opencode'],
|
||||
};
|
||||
fs.unlinkSync(configPath);
|
||||
const repaired = repairInstalledStates(lifecycleOptions);
|
||||
assert.strictEqual(repaired.results[0].status, 'repaired', repaired.results[0].error);
|
||||
assert.deepStrictEqual(JSON.parse(fs.readFileSync(configPath, 'utf8')), expected);
|
||||
const state = JSON.parse(fs.readFileSync(plan.installStatePath, 'utf8'));
|
||||
const recordedConfig = state.operations.find(operation => operation.destinationPath === configPath);
|
||||
assert.strictEqual(recordedConfig.contentSha256, crypto.createHash('sha256')
|
||||
.update(fs.readFileSync(configPath)).digest('hex'));
|
||||
const uninstalled = uninstallInstalledStates(lifecycleOptions);
|
||||
assert.strictEqual(uninstalled.results[0].status, 'uninstalled', uninstalled.results[0].error);
|
||||
assert.ok(!fs.existsSync(configPath));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cleanup(tempDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('dedupeCopyFileOperations keeps the last writer per destination (issue #2414)', () => {
|
||||
// Mirrors the OpenCode command scenario: a generic commands/<name>.md source
|
||||
// (preserve-relative-path) and an override .opencode/commands/<name>.md source
|
||||
|
||||
@@ -6,7 +6,57 @@ const assert = require("assert")
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { spawnSync } = require("child_process")
|
||||
const { pathToFileURL } = require("url")
|
||||
const { getNpmPackEntry } = require("../lib/npm-pack-output")
|
||||
const { createManifestInstallPlan, applyInstallPlan } = require("../../scripts/lib/install-executor")
|
||||
const { withHookConsent } = require("../../scripts/lib/install/hook-consent")
|
||||
|
||||
function checkRuntime(runtimeRoot, extension, loader) {
|
||||
const check = `
|
||||
const assert = require("node:assert/strict")
|
||||
const path = require("node:path")
|
||||
const { pathToFileURL } = require("node:url")
|
||||
const [root, extension] = process.argv.slice(1)
|
||||
const load = (name) => import(pathToFileURL(path.join(root, name + extension)).href)
|
||||
|
||||
async function main() {
|
||||
const tools = await load("tools/index")
|
||||
assert.deepEqual(Object.keys(tools).sort(), [
|
||||
"changedFiles", "checkCoverage", "dependencyAnalyzer", "formatCode",
|
||||
"gitSummary", "lintCheck", "runTests", "securityAudit",
|
||||
])
|
||||
for (const tool of Object.values(tools)) {
|
||||
assert.equal(typeof tool.description, "string")
|
||||
assert.equal(typeof tool.execute, "function")
|
||||
}
|
||||
|
||||
const plugins = await load("plugins/index")
|
||||
const entry = await load("index")
|
||||
assert.deepEqual(Object.keys(entry), ["default"])
|
||||
assert.equal(entry.default, plugins.default)
|
||||
const logs = []
|
||||
const plugin = await entry.default({
|
||||
client: { app: { log: (event) => logs.push(event.body) } },
|
||||
$: () => { throw new Error("Unexpected shell execution") },
|
||||
directory: process.cwd(),
|
||||
worktree: process.cwd(),
|
||||
})
|
||||
assert.equal(typeof plugin["session.created"], "function")
|
||||
await plugin["file.edited"]({ path: path.join(process.cwd(), "example.txt") })
|
||||
const changed = JSON.parse(await tools.changedFiles.execute({ format: "json" }, {}))
|
||||
assert.equal(changed.changed, true)
|
||||
assert.deepEqual(changed.files, [{ path: "example.txt", changeType: "modified" }])
|
||||
assert.ok(!logs.some((log) => log.message.includes("tracking disabled")))
|
||||
}
|
||||
main().catch((error) => { console.error(error); process.exit(1) })
|
||||
`
|
||||
const args = loader ? ["--loader", pathToFileURL(loader).href] : []
|
||||
const result = spawnSync(process.execPath, [...args, "-e", check, runtimeRoot, extension], {
|
||||
cwd: path.join(__dirname, "..", ".."),
|
||||
encoding: "utf8",
|
||||
})
|
||||
assert.strictEqual(result.status, 0, result.error?.message || result.stderr || result.stdout)
|
||||
}
|
||||
|
||||
function runTest(name, fn) {
|
||||
try {
|
||||
@@ -116,6 +166,40 @@ function main() {
|
||||
})
|
||||
assert.strictEqual(result.status, 0, result.stderr)
|
||||
}],
|
||||
["compiled barrels load and share the lazy changed-files store", () => {
|
||||
checkRuntime(path.dirname(distEntry), ".js")
|
||||
}],
|
||||
["full home install loads TypeScript barrels and the lazy changed-files store", () => {
|
||||
// Keep the fixture beneath the repo so the installed tools can resolve
|
||||
// the real SDK dependency without installing packages in the user's home.
|
||||
const fixture = fs.mkdtempSync(path.join(repoRoot, "tests", ".opencode-home-"))
|
||||
try {
|
||||
const homeDir = path.join(fixture, "home")
|
||||
const projectRoot = path.join(fixture, "project")
|
||||
fs.mkdirSync(homeDir)
|
||||
fs.mkdirSync(projectRoot)
|
||||
const plan = withHookConsent(createManifestInstallPlan({
|
||||
sourceRoot: repoRoot,
|
||||
homeDir,
|
||||
projectRoot,
|
||||
env: {},
|
||||
target: "opencode",
|
||||
profileId: "full",
|
||||
}), "enabled")
|
||||
applyInstallPlan(plan)
|
||||
const config = JSON.parse(fs.readFileSync(path.join(plan.targetRoot, "opencode.json"), "utf8"))
|
||||
assert.deepStrictEqual(config.skills.paths, ["./skills"])
|
||||
assert.ok(fs.existsSync(path.join(plan.targetRoot, config.skills.paths[0], "tdd-workflow", "SKILL.md")))
|
||||
assert.ok(!fs.existsSync(path.join(plan.targetRoot, "tools", "run-tests.js")))
|
||||
checkRuntime(
|
||||
plan.targetRoot,
|
||||
".ts",
|
||||
path.join(repoRoot, "tests", "fixtures", "opencode-ts-loader.mjs"),
|
||||
)
|
||||
} finally {
|
||||
fs.rmSync(fixture, { recursive: true, force: true })
|
||||
}
|
||||
}],
|
||||
["npm pack includes the compiled OpenCode dist payload", () => {
|
||||
const result = spawnSync("npm", ["pack", "--dry-run", "--json"], {
|
||||
cwd: repoRoot,
|
||||
|
||||
Reference in New Issue
Block a user