Merge reviewed OpenCode JSONC fix from #3590

This commit is contained in:
haelyra committed 2026-10-25 18:17:10 -04:00
commit f4e8f6eb3e
3 files changed
+161 -7

No files matched your search

+11 -4
View File
@@ -11,7 +11,7 @@ const {
const { readInstallState, writeInstallState } = require('../install-state');
const {
assertHookConsentReady,
disableOpenCodeHookPluginRegistration,
parseJsoncConfig,
getDisabledOpenCodePluginContent,
getRecordedHookConsent,
getOpenCodeActivationPathKind,
@@ -451,9 +451,16 @@ function activationIsInactive(kind, operation, content) {
return content.toString('utf8') === getDisabledOpenCodePluginContent();
}
const text = content.toString('utf8');
// Validate first so malformed JSON retains its source context.
disableOpenCodeHookPluginRegistration(text, operation.sourceRelativePath || operation.destinationPath);
const config = JSON.parse(text);
const relativePath = operation.sourceRelativePath || operation.destinationPath;
// JSONC-tolerant parse so a comment in the user's config does not crash
// the install; the ownership guard never writes this file (issue #3546).
const config = parseJsoncConfig(text, relativePath);
// Keep the same shape error the strict parser surfaced via
// disableOpenCodeHookPluginRegistration: a non-array plugin must not
// silently count as inactive.
if (config.plugin !== undefined && !Array.isArray(config.plugin)) {
throw new Error(`Invalid ${relativePath}: plugin must be an array`);
}
return !Array.isArray(config.plugin) || !config.plugin.includes('./plugins');
}
+88 -2
View File
@@ -45,16 +45,101 @@ function normalizeOperationPath(value) {
return String(value || '').replace(/\\/g, '/').toLowerCase();
}
function disableOpenCodeHookPluginRegistration(content, sourceRelativePath) {
/**
* Parse an OpenCode config the way OpenCode itself does: comments and the
* trailing commas they hide are legal (JSONC). The installer only ever
* reads these files (the ownership guard never writes user-owned configs),
* so a tolerant read prevents a membership check from crashing the install
* on a config OpenCode accepts.
*/
function parseJsoncConfig(content, sourceRelativePath) {
let config;
try {
config = JSON.parse(content);
config = JSON.parse(stripJsonc(content));
} catch (error) {
throw new Error(`Failed to parse ${sourceRelativePath}: ${error.message}`);
}
if (!config || typeof config !== 'object' || Array.isArray(config)) {
throw new Error(`Invalid ${sourceRelativePath}: expected a JSON object`);
}
return config;
}
function isLineBreak(ch) {
return ch === '\n' || ch === '\r';
}
/**
* Advance past whitespace and // or /* *\/ comments; returns the index of
* the next significant character.
*/
function skipJsoncTrivia(input, start) {
let j = start;
for (;;) {
while (j < input.length && /\s/.test(input[j])) j += 1;
if (input[j] === '/' && input[j + 1] === '/') {
while (j < input.length && !isLineBreak(input[j])) j += 1;
continue;
}
if (input[j] === '/' && input[j + 1] === '*') {
j += 2;
while (j < input.length && !(input[j] === '*' && input[j + 1] === '/')) j += 1;
if (j >= input.length) throw new Error('Unterminated block comment');
j += 2;
continue;
}
return j;
}
}
/**
* Strip // and /* *\/ comments and the trailing commas they hide, without
* touching string contents (URLs like "https://..." must survive intact).
*/
function stripJsonc(input) {
let out = '';
let inString = false;
let escaped = false;
for (let i = 0; i < input.length; i += 1) {
const ch = input[i];
if (inString) {
out += ch;
if (escaped) escaped = false;
else if (ch === '\\') escaped = true;
else if (ch === '"') inString = false;
continue;
}
if (ch === '"') {
inString = true;
out += ch;
continue;
}
if (ch === '/' && input[i + 1] === '/') {
while (i < input.length && !isLineBreak(input[i])) i += 1;
out += '\n';
continue;
}
if (ch === '/' && input[i + 1] === '*') {
i += 2;
while (i < input.length && !(input[i] === '*' && input[i + 1] === '/')) i += 1;
if (i >= input.length) throw new Error('Unterminated block comment');
i += 1; // consume the closing '/'
out += ' ';
continue;
}
if (ch === ',') {
const next = skipJsoncTrivia(input, i + 1);
// A comma before '}' or ']' (ignoring trivia) is a trailing comma
// JSON forbids.
if (input[next] === '}' || input[next] === ']') continue;
}
out += ch;
}
return out;
}
function disableOpenCodeHookPluginRegistration(content, sourceRelativePath) {
const config = parseJsoncConfig(content, sourceRelativePath);
if (config.plugin !== undefined && !Array.isArray(config.plugin)) {
throw new Error(`Invalid ${sourceRelativePath}: plugin must be an array`);
}
@@ -328,6 +413,7 @@ module.exports = {
assertHookConsentReady,
disableUnselectedOpenCodeHooks,
disableOpenCodeHookPluginRegistration,
parseJsoncConfig,
getDisabledOpenCodePluginContent,
getOpenCodeActivationPathKind,
getOpenCodeSourceActivationKind,