mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-16 05:05:30 +02:00
fix: resolve installed OpenCode skill paths consistently
Carry forward PR #3115ef475c79and780dfb78onto current main. Preserve the newer hook-consent transforms during install and repair. Main already includes the source import fix from #3167.
This commit is contained in:
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"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* These tools extend OpenCode with additional capabilities.
|
||||
*/
|
||||
|
||||
// Re-export all tools
|
||||
// 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"
|
||||
|
||||
@@ -14,8 +14,6 @@ const { writeFileNoFollow: guardedWriteFile } = require('./install/guarded-write
|
||||
const { withOpenCodeInstallLocks } = require('./install/opencode-install-lock');
|
||||
const { isCodexUserConfig } = require('./install/codex-user-config');
|
||||
const {
|
||||
disableOpenCodeHookPluginRegistration,
|
||||
getDisabledOpenCodePluginContent,
|
||||
getRecordedHookConsent,
|
||||
withHookConsent,
|
||||
} = require('./install/hook-consent');
|
||||
@@ -41,7 +39,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');
|
||||
@@ -232,24 +230,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);
|
||||
}
|
||||
if (operation.contentTransform === 'opencode-disable-ecc-hooks') {
|
||||
return disableOpenCodeHookPluginRegistration(content, operation.sourceRelativePath);
|
||||
}
|
||||
if (operation.contentTransform === 'opencode-disable-plugin-entrypoint') {
|
||||
return getDisabledOpenCodePluginContent();
|
||||
}
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -51,28 +51,12 @@ const {
|
||||
prepareExcludedPathsReconciliation,
|
||||
} = require('./excluded-paths-reconciliation');
|
||||
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);
|
||||
}
|
||||
if (operation.contentTransform === 'opencode-disable-ecc-hooks') {
|
||||
return disableOpenCodeHookPluginRegistration(content, operation.sourceRelativePath);
|
||||
}
|
||||
if (operation.contentTransform === 'opencode-disable-plugin-entrypoint') {
|
||||
return getDisabledOpenCodePluginContent();
|
||||
}
|
||||
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,37 @@
|
||||
'use strict';
|
||||
|
||||
const { adaptAntigravityAgent } = require('./antigravity-agent');
|
||||
const { disableOpenCodeHookPluginRegistration, getDisabledOpenCodePluginContent } = require('./hook-consent');
|
||||
|
||||
function transformInstallContent(operation, content) {
|
||||
if (!operation.contentTransform) {
|
||||
return content;
|
||||
}
|
||||
if (operation.contentTransform === 'antigravity-agent-frontmatter') {
|
||||
return adaptAntigravityAgent(content, operation.sourceRelativePath);
|
||||
}
|
||||
if (operation.contentTransform === 'opencode-disable-ecc-hooks') {
|
||||
return transformInstallContent({ ...operation, contentTransform: 'opencode-home-skills-path' },
|
||||
disableOpenCodeHookPluginRegistration(content, operation.sourceRelativePath));
|
||||
}
|
||||
if (operation.contentTransform === 'opencode-disable-plugin-entrypoint') {
|
||||
return getDisabledOpenCodePluginContent();
|
||||
}
|
||||
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 }
|
||||
}
|
||||
@@ -23,6 +23,7 @@ const { applyInstallPlan: applyInstallPlanDirect } = require('../../scripts/lib/
|
||||
const { normalizeInstallRequest } = require('../../scripts/lib/install/request');
|
||||
const { createInstallPlanFromRequest } = require('../../scripts/lib/install/runtime');
|
||||
const { withHookConsent } = require('../../scripts/lib/install/hook-consent');
|
||||
const { repairInstalledStates, uninstallInstalledStates } = require('../../scripts/lib/install-lifecycle');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
||||
|
||||
@@ -745,6 +746,174 @@ 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 dormantConfig = { ...config, plugin: config.plugin.filter(plugin => plugin !== './plugins') };
|
||||
const expected = index === 0
|
||||
? { ...dormantConfig, skills: { ...config.skills, paths: ['./skills', '/custom/skills', '../other-skills'] } }
|
||||
: dormantConfig;
|
||||
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('OpenCode pre-fix managed sources upgrade on repair and uninstall preserves user changes', () => {
|
||||
const tempDir = createTempDir('install-executor-opencode-upgrade-');
|
||||
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'],
|
||||
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');
|
||||
writeJson(sourceRoot, '.opencode/opencode.json', { plugin: ['./plugins'] });
|
||||
fs.cpSync(path.join(REPO_ROOT, '.opencode', 'tools'), path.join(sourceRoot, '.opencode', 'tools'), {
|
||||
recursive: true,
|
||||
});
|
||||
const currentContent = fs.readFileSync(path.join(REPO_ROOT, '.opencode', 'tools', 'index.ts'), 'utf8');
|
||||
const legacyContent = [
|
||||
'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"',
|
||||
'',
|
||||
].join('\n');
|
||||
const digest = content => crypto.createHash('sha256').update(content).digest('hex');
|
||||
|
||||
for (const upgrade of [false, true]) {
|
||||
writeFile(sourceRoot, '.opencode/tools/index.ts', legacyContent);
|
||||
const homeDir = path.join(tempDir, upgrade ? 'repair-home' : 'uninstall-home');
|
||||
const projectRoot = path.join(tempDir, 'project');
|
||||
const plan = createManifestInstallPlan({
|
||||
sourceRoot, projectRoot, homeDir, target: 'opencode', moduleIds: ['fixture-core'],
|
||||
});
|
||||
applyInstallPlan(plan);
|
||||
const barrelPath = path.join(plan.targetRoot, 'tools', 'index.ts');
|
||||
const toolPath = path.join(plan.targetRoot, 'tools', 'run-tests.ts');
|
||||
const userPath = writeFile(plan.targetRoot, 'tools/user-note.txt', 'Keep my settings.\n');
|
||||
const installedState = JSON.parse(fs.readFileSync(plan.installStatePath, 'utf8'));
|
||||
const recordedBarrel = installedState.operations.find(operation => operation.destinationPath === barrelPath);
|
||||
assert.strictEqual(fs.readFileSync(barrelPath, 'utf8'), legacyContent);
|
||||
assert.strictEqual(recordedBarrel.contentTransform, undefined, 'TypeScript imports are copied, not transformed');
|
||||
assert.strictEqual(recordedBarrel.contentSha256, digest(legacyContent));
|
||||
assert.ok(fs.existsSync(toolPath));
|
||||
assert.ok(!fs.existsSync(path.join(plan.targetRoot, 'tools', 'run-tests.js')), 'legacy import has no adjacent JavaScript file');
|
||||
|
||||
// Upgrade the repository independently of the existing managed home.
|
||||
writeFile(sourceRoot, '.opencode/tools/index.ts', currentContent);
|
||||
const lifecycleOptions = { repoRoot: sourceRoot, projectRoot, homeDir, targets: ['opencode'] };
|
||||
if (upgrade) {
|
||||
const repaired = repairInstalledStates(lifecycleOptions);
|
||||
assert.strictEqual(repaired.results.length, 1);
|
||||
assert.strictEqual(repaired.results[0].status, 'repaired', JSON.stringify(repaired));
|
||||
assert.ok(repaired.results[0].repairedPaths.includes(barrelPath));
|
||||
const repairedContent = fs.readFileSync(barrelPath, 'utf8');
|
||||
assert.strictEqual(repairedContent, currentContent);
|
||||
assert.ok(repairedContent.includes('from "./run-tests.ts"'));
|
||||
assert.ok(!repairedContent.includes('.js"'), 'repair replaces legacy JavaScript specifiers');
|
||||
const repairedState = JSON.parse(fs.readFileSync(plan.installStatePath, 'utf8'));
|
||||
const repairedBarrel = repairedState.operations.find(operation => operation.destinationPath === barrelPath);
|
||||
assert.strictEqual(repairedBarrel.contentSha256, digest(currentContent));
|
||||
assert.notStrictEqual(repairedBarrel.contentSha256, recordedBarrel.contentSha256);
|
||||
assert.strictEqual(fs.readFileSync(userPath, 'utf8'), 'Keep my settings.\n');
|
||||
}
|
||||
|
||||
const modifiedContent = `${fs.readFileSync(toolPath, 'utf8')}\n// User test customisation.\n`;
|
||||
fs.writeFileSync(toolPath, modifiedContent, 'utf8');
|
||||
const uninstalled = uninstallInstalledStates(lifecycleOptions);
|
||||
assert.strictEqual(uninstalled.results.length, 1);
|
||||
assert.strictEqual(uninstalled.results[0].status, 'partial', JSON.stringify(uninstalled));
|
||||
assert.ok(uninstalled.results[0].removedPaths.includes(barrelPath));
|
||||
assert.ok(!fs.existsSync(barrelPath), 'both legacy and repaired managed barrels are recognised');
|
||||
assert.deepStrictEqual(uninstalled.results[0].retainedPaths, [toolPath]);
|
||||
assert.strictEqual(fs.readFileSync(toolPath, 'utf8'), modifiedContent);
|
||||
assert.strictEqual(fs.readFileSync(userPath, 'utf8'), 'Keep my settings.\n');
|
||||
assert.ok(fs.existsSync(plan.installStatePath), 'modified managed content retains its ownership record');
|
||||
}
|
||||
} 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
|
||||
@@ -934,6 +1103,7 @@ function runTests() {
|
||||
'utf8'
|
||||
));
|
||||
assert.ok(!installedConfig.plugin.includes('./plugins'));
|
||||
assert.deepStrictEqual(installedConfig.skills.paths, ['./skills']);
|
||||
|
||||
const enabledWithoutRuntime = createInstallPlanFromRequest(
|
||||
normalizeInstallRequest({
|
||||
@@ -985,8 +1155,9 @@ function runTests() {
|
||||
const enabledConfig = enabledPlan.operations.find(operation => (
|
||||
operation.sourceRelativePath.split(path.sep).join('/') === '.opencode/opencode.json'
|
||||
));
|
||||
assert.strictEqual(enabledConfig.contentTransform, undefined);
|
||||
assert.strictEqual(enabledConfig.contentTransform, 'opencode-home-skills-path');
|
||||
applyInstallPlanDirect(enabledPlan);
|
||||
assert.deepStrictEqual(JSON.parse(fs.readFileSync(enabledConfig.destinationPath, 'utf8')).skills.paths, ['./skills']);
|
||||
for (const name of OPENCODE_ENTRYPOINTS) {
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(path.join(homeDir, '.config', 'opencode', 'plugins', name), 'utf8'),
|
||||
|
||||
@@ -1997,6 +1997,7 @@ function runTests() {
|
||||
const expectedContent = `${JSON.stringify({
|
||||
...sourceConfig,
|
||||
plugin: sourceConfig.plugin.filter(plugin => plugin !== './plugins'),
|
||||
skills: { ...sourceConfig.skills, paths: ['./skills'] },
|
||||
}, null, 2)}\n`;
|
||||
fs.mkdirSync(targetRoot, { recursive: true });
|
||||
fs.writeFileSync(destinationPath, expectedContent, 'utf8');
|
||||
|
||||
@@ -7,7 +7,57 @@ const fs = require("fs")
|
||||
const os = require("os")
|
||||
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 {
|
||||
@@ -183,6 +233,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", () => {
|
||||
fs.rmSync(path.dirname(distEntry), { recursive: true, force: true })
|
||||
const result = spawnSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts=false"], {
|
||||
|
||||
Reference in New Issue
Block a user