mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-26 18:35:05 +00:00
fix(opencode): preserve CommonJS runtime scope and user script ownership
This commit is contained in:
8 files changed
+363
-3
No files matched your search
@@ -1727,6 +1727,7 @@ function repairInstalledStates(options = {}) {
|
||||
}
|
||||
|
||||
const rawPlan = createRepairPlanFromRecord(record, context);
|
||||
require('./install/apply').assertOpenCodeScriptBoundary(rawPlan);
|
||||
const {
|
||||
migration,
|
||||
plan: desiredPlan,
|
||||
@@ -1811,6 +1812,9 @@ function repairInstalledStates(options = {}) {
|
||||
}
|
||||
|
||||
for (const operation of repairOperations) {
|
||||
if (operation.sourceRelativePath === 'manifests/install-assets/commonjs-scripts-package.json') {
|
||||
require('./install/apply').assertOpenCodeScriptBoundary(desiredPlan, new Set(repairedPaths));
|
||||
}
|
||||
if (desiredPlan.target === 'opencode') {
|
||||
const { assertOpenCodeActivationUnchanged } = require('./install/apply');
|
||||
assertOpenCodeActivationUnchanged(desiredPlan, operation, activationSnapshot);
|
||||
@@ -1996,7 +2000,18 @@ function uninstallInstalledStates(options = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
for (const operation of operations) {
|
||||
const { isScriptBoundary, retainScriptBoundary } = require('./install/opencode-script-boundary');
|
||||
// Remove owned script bodies first. Keep their CommonJS scope while any
|
||||
// retained or user-created JavaScript still relies on that boundary.
|
||||
const orderedOperations = record.adapter.target === 'opencode'
|
||||
? [...operations.filter(operation => !isScriptBoundary(operation, record.targetRoot)),
|
||||
...operations.filter(operation => isScriptBoundary(operation, record.targetRoot))]
|
||||
: operations;
|
||||
for (const operation of orderedOperations) {
|
||||
if (record.adapter.target === 'opencode' && retainScriptBoundary(operation, record.targetRoot)) {
|
||||
retainedPaths.push(operation.destinationPath);
|
||||
continue;
|
||||
}
|
||||
const outcome = executeUninstallOperation(operation, record.targetRoot, {
|
||||
preserveDriftedCopies: true,
|
||||
target: record.adapter.target,
|
||||
|
||||
@@ -94,7 +94,7 @@ module.exports = createInstallTargetAdapter({
|
||||
const modules = Array.isArray(input.modules)
|
||||
? input.modules
|
||||
: (input.module ? [input.module] : []);
|
||||
return modules.flatMap(module => (Array.isArray(module.paths) ? module.paths : [])
|
||||
const operations = 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);
|
||||
@@ -111,6 +111,18 @@ module.exports = createInstallTargetAdapter({
|
||||
contentTransform: 'opencode-home-skills-path',
|
||||
})];
|
||||
}));
|
||||
const scriptOperation = operations.find(operation => (
|
||||
String(operation.sourceRelativePath).replace(/\\/g, '/').startsWith('scripts/')
|
||||
));
|
||||
if (!scriptOperation) return operations;
|
||||
// OpenCode's root is ESM, while ECC runtime scripts remain CommonJS.
|
||||
// Record this boundary so install, repair and uninstall share ownership.
|
||||
return [...operations, createManagedOperation({
|
||||
moduleId: scriptOperation.moduleId,
|
||||
sourceRelativePath: 'manifests/install-assets/commonjs-scripts-package.json',
|
||||
destinationPath: path.join(adapter.resolveRoot(input), 'scripts', 'package.json'),
|
||||
strategy: 'preserve-relative-path',
|
||||
})];
|
||||
},
|
||||
validate: defaultValidateOpencodeHome,
|
||||
});
|
||||
@@ -58,6 +58,7 @@ const {
|
||||
} = require('./stale-operations-reconciliation');
|
||||
const { buildInstallIndex, rewriteRelativeLinks } = require('./link-rewrite');
|
||||
const { transformInstallContent } = require('./content-transform');
|
||||
const { assertScriptBoundary } = require('./opencode-script-boundary');
|
||||
|
||||
function isMarkdownPath(filePath) {
|
||||
return /\.(md|mdx|markdown)$/i.test(String(filePath || ''));
|
||||
@@ -295,6 +296,22 @@ function readPreviousInstallState(plan) {
|
||||
return readInstallState(plan.installStatePath);
|
||||
}
|
||||
|
||||
function assertOpenCodeScriptBoundary(plan, writtenDestinations = new Set()) {
|
||||
if (plan.adapter?.target !== 'opencode') return;
|
||||
assertSafeInstallOperation(plan, { destinationPath: plan.installStatePath });
|
||||
assertScriptBoundary(plan, {
|
||||
readFile: destinationPath => readInstalledFileNoFollow(plan, { destinationPath }),
|
||||
previousOperations: readPreviousInstallState(plan)?.operations || [],
|
||||
writtenDestinations,
|
||||
expectedContent(operation) {
|
||||
if (!operation || operation.kind !== 'copy-file') return null;
|
||||
const source = fs.readFileSync(operation.sourcePath);
|
||||
return operation.contentTransform
|
||||
? Buffer.from(transformInstallContent(operation, source.toString('utf8'))) : source;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function comparablePath(filePath) {
|
||||
const resolved = path.resolve(filePath);
|
||||
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
||||
@@ -690,6 +707,7 @@ function preflightQwenAgentOperations(plan) {
|
||||
|
||||
function previewInstallPlan(plan) {
|
||||
preflightQwenAgentOperations(plan);
|
||||
assertOpenCodeScriptBoundary(plan);
|
||||
assertOpenCodeHookDeactivationReady(plan);
|
||||
const migration = prepareStaleOperationsReconciliation(
|
||||
plan,
|
||||
@@ -757,6 +775,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
|
||||
if (typeof beforeInstallStateRead === 'function') {
|
||||
beforeInstallStateRead({ plan });
|
||||
}
|
||||
assertOpenCodeScriptBoundary(plan);
|
||||
assertOpenCodeLeaseCoverage(plan, dependencies.opencodeLease);
|
||||
const legacyActivation = inspectLegacyOpenCodeDeactivation(plan);
|
||||
const activationSnapshot = assertOpenCodeHookDeactivationReady(plan);
|
||||
@@ -809,6 +828,9 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
|
||||
beforeOperationWrite({ plan: appliedPlan, operation });
|
||||
}
|
||||
assertNoNewUserOwnedFile(migration, operation, appliedPlan);
|
||||
if (operation.sourceRelativePath === 'manifests/install-assets/commonjs-scripts-package.json') {
|
||||
assertOpenCodeScriptBoundary(appliedPlan, writtenDestinations);
|
||||
}
|
||||
assertOpenCodeActivationUnchanged(appliedPlan, operation, activationSnapshot);
|
||||
|
||||
if (
|
||||
@@ -1059,6 +1081,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
|
||||
|
||||
module.exports = {
|
||||
applyInstallPlan,
|
||||
assertOpenCodeScriptBoundary,
|
||||
assertOpenCodeActivationUnchanged,
|
||||
assertOpenCodeLeaseCoverage,
|
||||
assertOpenCodeHookDeactivationReady,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SOURCE = 'manifests/install-assets/commonjs-scripts-package.json';
|
||||
const key = file => process.platform === 'win32' ? path.resolve(file).toLowerCase() : path.resolve(file);
|
||||
const markerPath = root => path.join(root, 'scripts', 'package.json');
|
||||
const isScriptBoundary = (operation, root) => operation.kind === 'copy-file'
|
||||
&& operation.sourceRelativePath === SOURCE && key(operation.destinationPath) === key(markerPath(root));
|
||||
|
||||
// A nested package owns its own module scope. Symlinks and uncertain trees are
|
||||
// preserved conservatively rather than followed while deciding scope changes.
|
||||
function scopedJavaScriptFiles(directory, { nested = false, depth = 0 } = {}) {
|
||||
if (depth > 40) throw new Error('OpenCode scripts tree exceeds the safe inspection depth');
|
||||
const stat = fs.lstatSync(directory, { throwIfNoEntry: false });
|
||||
if (!stat) return [];
|
||||
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`Unsafe OpenCode scripts directory: ${directory}`);
|
||||
const packageStat = fs.lstatSync(path.join(directory, 'package.json'), { throwIfNoEntry: false });
|
||||
if (nested && packageStat?.isFile()) return [];
|
||||
const files = [];
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
const file = path.join(directory, entry.name);
|
||||
if (entry.isSymbolicLink()) throw new Error(`Unsafe OpenCode scripts symlink: ${file}`);
|
||||
if (entry.isDirectory()) files.push(...scopedJavaScriptFiles(file, { nested: true, depth: depth + 1 }));
|
||||
else if (entry.isFile() && entry.name.toLowerCase().endsWith('.js')) files.push(file);
|
||||
if (files.length > 12000) throw new Error('OpenCode scripts tree exceeds the safe inspection count');
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function assertScriptBoundary(plan, { readFile, previousOperations = [], writtenDestinations = new Set(), expectedContent }) {
|
||||
if (plan.adapter?.target !== 'opencode'
|
||||
|| !plan.operations.some(operation => isScriptBoundary(operation, plan.targetRoot))) return;
|
||||
const marker = markerPath(plan.targetRoot);
|
||||
const content = readFile(marker);
|
||||
if (content !== null) {
|
||||
const metadata = JSON.parse(content.toString('utf8'));
|
||||
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)
|
||||
|| (metadata.type !== undefined && metadata.type !== 'commonjs')) {
|
||||
throw new Error(`Refusing OpenCode CommonJS runtime migration: preserve the existing user module boundary at ${marker}.`);
|
||||
}
|
||||
// Existing user-owned CommonJS metadata is skipped by the ownership guard;
|
||||
// retaining its bytes does not change anyone's module interpretation.
|
||||
return;
|
||||
}
|
||||
const previous = new Map(previousOperations.filter(operation => operation.ownership === 'managed')
|
||||
.map(operation => [key(operation.destinationPath), operation]));
|
||||
const written = new Set([...writtenDestinations].map(key));
|
||||
const planned = new Map(plan.operations.map(operation => [key(operation.destinationPath), operation]));
|
||||
for (const file of scopedJavaScriptFiles(path.dirname(marker))) {
|
||||
const operation = previous.get(key(file));
|
||||
const bytes = readFile(file);
|
||||
const expected = written.has(key(file))
|
||||
? expectedContent(planned.get(key(file))) : null;
|
||||
const digest = bytes === null ? null : crypto.createHash('sha256').update(bytes).digest('hex');
|
||||
const expectedDigest = expected === null
|
||||
? operation?.contentSha256 : crypto.createHash('sha256').update(expected).digest('hex');
|
||||
if (!expectedDigest || digest !== expectedDigest) {
|
||||
throw new Error(`Refusing OpenCode CommonJS runtime migration: unowned or edited JavaScript at ${file} would change module scope. Give user scripts their own package.json scope before retrying.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function retainScriptBoundary(operation, targetRoot) {
|
||||
if (!isScriptBoundary(operation, targetRoot)) return false;
|
||||
try { return scopedJavaScriptFiles(path.dirname(operation.destinationPath)).length > 0; }
|
||||
catch { return true; }
|
||||
}
|
||||
|
||||
module.exports = { assertScriptBoundary, isScriptBoundary, retainScriptBoundary };
|
||||
Reference in new issue
Block a user