mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-30 13:45:21 +08:00
fix(install-targets): validate compiled OpenCode plugin before install (#2041)
Fail fast when the OpenCode home install is attempted from a source checkout without the compiled .opencode/dist payload. PR had the full CI matrix green.
This commit is contained in:
parent
3ffab636ad
commit
ee9e5a19c4
@ -57,6 +57,19 @@ cd ECC
|
|||||||
opencode
|
opencode
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you also want to apply the ECC home install
|
||||||
|
(`node scripts/install-apply.js --target opencode --profile full`), build the
|
||||||
|
plugin first so the compiled payload at `.opencode/dist/` exists:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/build-opencode.js # or: npm run build:opencode
|
||||||
|
node scripts/install-apply.js --target opencode --profile full
|
||||||
|
```
|
||||||
|
|
||||||
|
Without `.opencode/dist/index.js`, OpenCode will detect the slash commands
|
||||||
|
but silently skip plugin hooks and tools. The installer now fails fast with
|
||||||
|
a pointer to this command if the build step is missing.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Agents (12)
|
### Agents (12)
|
||||||
|
|||||||
@ -1,4 +1,83 @@
|
|||||||
const { createInstallTargetAdapter } = require('./helpers');
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const {
|
||||||
|
buildValidationIssue,
|
||||||
|
createInstallTargetAdapter,
|
||||||
|
} = require('./helpers');
|
||||||
|
|
||||||
|
const COMPILED_PLUGIN_DIST_DIR = path.join('.opencode', 'dist');
|
||||||
|
const REQUIRED_COMPILED_ARTEFACTS = Object.freeze([
|
||||||
|
{ relativePath: path.join(COMPILED_PLUGIN_DIST_DIR, 'index.js'), expectedType: 'file' },
|
||||||
|
{ relativePath: path.join(COMPILED_PLUGIN_DIST_DIR, 'plugins'), expectedType: 'directory' },
|
||||||
|
{ relativePath: path.join(COMPILED_PLUGIN_DIST_DIR, 'tools'), expectedType: 'directory' },
|
||||||
|
]);
|
||||||
|
const BUILD_COMMAND_HINT = 'node scripts/build-opencode.js (or: npm run build:opencode)';
|
||||||
|
|
||||||
|
// Errors that mean "this artefact does not exist at the expected path / type".
|
||||||
|
// Anything else (EACCES, EIO, ...) is a genuine system fault we surface to the
|
||||||
|
// caller rather than masking as a missing artefact.
|
||||||
|
const MISSING_ARTEFACT_ERROR_CODES = new Set(['ENOENT', 'ENOTDIR']);
|
||||||
|
|
||||||
|
function isExpectedType(absolutePath, expectedType) {
|
||||||
|
let stat;
|
||||||
|
try {
|
||||||
|
stat = fs.statSync(absolutePath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error && MISSING_ARTEFACT_ERROR_CODES.has(error.code)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return expectedType === 'file' ? stat.isFile() : stat.isDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultValidateOpencodeHome(input = {}) {
|
||||||
|
if (!input.homeDir && !os.homedir()) {
|
||||||
|
return [
|
||||||
|
buildValidationIssue(
|
||||||
|
'error',
|
||||||
|
'missing-home-dir',
|
||||||
|
'homeDir is required for home install targets'
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.repoRoot) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingPaths = REQUIRED_COMPILED_ARTEFACTS
|
||||||
|
.map(artefact => ({
|
||||||
|
relativePath: artefact.relativePath,
|
||||||
|
absolutePath: path.join(input.repoRoot, artefact.relativePath),
|
||||||
|
expectedType: artefact.expectedType,
|
||||||
|
}))
|
||||||
|
.filter(entry => !isExpectedType(entry.absolutePath, entry.expectedType));
|
||||||
|
|
||||||
|
if (missingPaths.length > 0) {
|
||||||
|
const missingList = missingPaths.map(entry => entry.relativePath).join(', ');
|
||||||
|
return [
|
||||||
|
buildValidationIssue(
|
||||||
|
'error',
|
||||||
|
'opencode-plugin-not-built',
|
||||||
|
'OpenCode install requires the compiled plugin payload under '
|
||||||
|
+ `${COMPILED_PLUGIN_DIST_DIR}, but the following artefact(s) were `
|
||||||
|
+ `missing or had the wrong type: ${missingList}. Run `
|
||||||
|
+ `${BUILD_COMMAND_HINT} from the repo root before re-running the `
|
||||||
|
+ 'installer.',
|
||||||
|
{
|
||||||
|
missingPaths: missingPaths.map(entry => entry.absolutePath),
|
||||||
|
missingRelativePaths: missingPaths.map(entry => entry.relativePath),
|
||||||
|
expectedTypes: missingPaths.map(entry => entry.expectedType),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = createInstallTargetAdapter({
|
module.exports = createInstallTargetAdapter({
|
||||||
id: 'opencode-home',
|
id: 'opencode-home',
|
||||||
@ -7,4 +86,5 @@ module.exports = createInstallTargetAdapter({
|
|||||||
rootSegments: ['.opencode'],
|
rootSegments: ['.opencode'],
|
||||||
installStatePathSegments: ['ecc-install-state.json'],
|
installStatePathSegments: ['ecc-install-state.json'],
|
||||||
nativeRootRelativePath: '.opencode',
|
nativeRootRelativePath: '.opencode',
|
||||||
|
validate: defaultValidateOpencodeHome,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -966,6 +968,132 @@ function runTests() {
|
|||||||
);
|
);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('resolves opencode adapter root and install-state path from home dir', () => {
|
||||||
|
const adapter = getInstallTargetAdapter('opencode');
|
||||||
|
const homeDir = '/Users/example';
|
||||||
|
const root = adapter.resolveRoot({ homeDir });
|
||||||
|
const statePath = adapter.getInstallStatePath({ homeDir });
|
||||||
|
|
||||||
|
assert.strictEqual(adapter.id, 'opencode-home');
|
||||||
|
assert.strictEqual(adapter.target, 'opencode');
|
||||||
|
assert.strictEqual(adapter.kind, 'home');
|
||||||
|
assert.strictEqual(root, path.join(homeDir, '.opencode'));
|
||||||
|
assert.strictEqual(statePath, path.join(homeDir, '.opencode', 'ecc-install-state.json'));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('opencode adapter validate reports an error when compiled plugin is missing', () => {
|
||||||
|
const adapter = getInstallTargetAdapter('opencode');
|
||||||
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'install-targets-opencode-missing-'));
|
||||||
|
try {
|
||||||
|
const issues = adapter.validate({ homeDir: '/Users/example', repoRoot });
|
||||||
|
assert.strictEqual(issues.length, 1, 'Should surface exactly one validation issue');
|
||||||
|
assert.strictEqual(issues[0].severity, 'error');
|
||||||
|
assert.strictEqual(issues[0].code, 'opencode-plugin-not-built');
|
||||||
|
assert.ok(
|
||||||
|
issues[0].message.includes('.opencode/dist') || issues[0].message.includes('.opencode\\dist'),
|
||||||
|
'Validation message should reference the .opencode/dist payload location'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
issues[0].message.includes('build-opencode.js') || issues[0].message.includes('build:opencode'),
|
||||||
|
'Validation message should hint at the build command'
|
||||||
|
);
|
||||||
|
assert.ok(Array.isArray(issues[0].missingRelativePaths) && issues[0].missingRelativePaths.length >= 1,
|
||||||
|
'Validation issue should expose the list of missing artefacts as metadata');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(repoRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('opencode adapter validate reports a partial build (entry present, runtime dirs absent)', () => {
|
||||||
|
const adapter = getInstallTargetAdapter('opencode');
|
||||||
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'install-targets-opencode-partial-'));
|
||||||
|
try {
|
||||||
|
const distDir = path.join(repoRoot, '.opencode', 'dist');
|
||||||
|
fs.mkdirSync(distDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(distDir, 'index.js'), '// stub\n');
|
||||||
|
// Intentionally omit dist/plugins and dist/tools.
|
||||||
|
|
||||||
|
const issues = adapter.validate({ homeDir: '/Users/example', repoRoot });
|
||||||
|
assert.strictEqual(issues.length, 1, 'Should surface a single validation issue for partial builds');
|
||||||
|
assert.strictEqual(issues[0].code, 'opencode-plugin-not-built');
|
||||||
|
const missing = issues[0].missingRelativePaths.map(p => p.replace(/\\/g, '/'));
|
||||||
|
assert.ok(missing.includes('.opencode/dist/plugins'), 'Missing list should include dist/plugins');
|
||||||
|
assert.ok(missing.includes('.opencode/dist/tools'), 'Missing list should include dist/tools');
|
||||||
|
assert.ok(!missing.includes('.opencode/dist/index.js'), 'Missing list should not include the present entry');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(repoRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('opencode adapter validate rejects wrong artefact type (file where directory expected)', () => {
|
||||||
|
const adapter = getInstallTargetAdapter('opencode');
|
||||||
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'install-targets-opencode-wrongtype-'));
|
||||||
|
try {
|
||||||
|
const distDir = path.join(repoRoot, '.opencode', 'dist');
|
||||||
|
fs.mkdirSync(distDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(distDir, 'index.js'), '// stub\n');
|
||||||
|
// Materialize plugins/tools as files instead of directories.
|
||||||
|
fs.writeFileSync(path.join(distDir, 'plugins'), 'not-a-dir');
|
||||||
|
fs.writeFileSync(path.join(distDir, 'tools'), 'not-a-dir');
|
||||||
|
|
||||||
|
const issues = adapter.validate({ homeDir: '/Users/example', repoRoot });
|
||||||
|
assert.strictEqual(issues.length, 1, 'Wrong-type artefacts should still surface a validation issue');
|
||||||
|
assert.strictEqual(issues[0].code, 'opencode-plugin-not-built');
|
||||||
|
const missing = issues[0].missingRelativePaths.map(p => p.replace(/\\/g, '/'));
|
||||||
|
assert.ok(missing.includes('.opencode/dist/plugins'), 'Should flag plugins file as wrong type');
|
||||||
|
assert.ok(missing.includes('.opencode/dist/tools'), 'Should flag tools file as wrong type');
|
||||||
|
assert.ok(!missing.includes('.opencode/dist/index.js'), 'Should not flag index.js when it is correctly a file');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(repoRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('opencode adapter validate handles ENOTDIR (intermediate path is a file) without throwing', () => {
|
||||||
|
const adapter = getInstallTargetAdapter('opencode');
|
||||||
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'install-targets-opencode-enotdir-'));
|
||||||
|
try {
|
||||||
|
// Create `.opencode/dist` as a regular file. Stat'ing
|
||||||
|
// `.opencode/dist/index.js` then throws ENOTDIR (intermediate component
|
||||||
|
// is a file, not a directory). The validate gate must treat this as a
|
||||||
|
// missing artefact and surface the structured opencode-plugin-not-built
|
||||||
|
// issue, not propagate the raw fs error.
|
||||||
|
const opencodeDir = path.join(repoRoot, '.opencode');
|
||||||
|
fs.mkdirSync(opencodeDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(opencodeDir, 'dist'), 'not-a-dir');
|
||||||
|
|
||||||
|
let issues;
|
||||||
|
assert.doesNotThrow(
|
||||||
|
() => { issues = adapter.validate({ homeDir: '/Users/example', repoRoot }); },
|
||||||
|
'validate should swallow ENOTDIR and surface a structured issue'
|
||||||
|
);
|
||||||
|
assert.strictEqual(issues.length, 1, 'ENOTDIR case should produce exactly one validation issue');
|
||||||
|
assert.strictEqual(issues[0].severity, 'error');
|
||||||
|
assert.strictEqual(issues[0].code, 'opencode-plugin-not-built');
|
||||||
|
const missing = issues[0].missingRelativePaths.map(p => p.replace(/\\/g, '/'));
|
||||||
|
assert.ok(missing.includes('.opencode/dist/index.js'), 'ENOTDIR target should be reported as missing');
|
||||||
|
assert.ok(missing.includes('.opencode/dist/plugins'), 'Sibling artefacts under the bad path should be reported');
|
||||||
|
assert.ok(missing.includes('.opencode/dist/tools'), 'Sibling artefacts under the bad path should be reported');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(repoRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('opencode adapter validate passes once compiled plugin payload exists', () => {
|
||||||
|
const adapter = getInstallTargetAdapter('opencode');
|
||||||
|
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'install-targets-opencode-built-'));
|
||||||
|
try {
|
||||||
|
const distDir = path.join(repoRoot, '.opencode', 'dist');
|
||||||
|
fs.mkdirSync(path.join(distDir, 'plugins'), { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(distDir, 'tools'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(distDir, 'index.js'), '// stub\n');
|
||||||
|
|
||||||
|
const issues = adapter.validate({ homeDir: '/Users/example', repoRoot });
|
||||||
|
assert.deepStrictEqual(issues, [], 'Should not surface validation issues when plugin is built');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(repoRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user