fix(cli-tools): tolerate JSONC configs in CLI tool settings routes

readConfig/readSettings/readJson across all CLI tool settings routes used
JSON.parse() but only caught ENOENT, re-throwing SyntaxError on any
corrupted or JSONC-formatted config file. The API would return 500, which
the UI misinterpreted as "tool not installed."

This patch:
- strips trailing commas before parsing (handles JSONC from opencode, etc.)
- returns null on any parse error instead of re-throwing, so the UI shows
  "installed but not configured" (accurate) instead of "not installed"
- applies the same fix to all 8 affected routes: opencode, openclaw, kilo,
  droid, cowork, copilot, claude, and cline

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Zireael 2026-06-20 15:34:23 +07:00 committed by decolua
parent f6c2f7cae3
commit 6c10edf8ba
8 changed files with 54 additions and 30 deletions

View file

@ -41,12 +41,12 @@ const readSettings = async () => {
try { try {
const settingsPath = getClaudeSettingsPath(); const settingsPath = getClaudeSettingsPath();
const content = await fs.readFile(settingsPath, "utf-8"); const content = await fs.readFile(settingsPath, "utf-8");
return JSON.parse(content); // Tolerate JSONC (trailing commas) and treat unparseable files as "no config"
// rather than throwing a 500 that the UI misreads as "tool not installed".
const stripped = content.replace(/,(\s*[}\]])/g, "$1");
return JSON.parse(stripped);
} catch (error) { } catch (error) {
if (error.code === "ENOENT") { return null;
return null;
}
throw error;
} }
}; };

View file

@ -35,10 +35,12 @@ const checkInstalled = async () => {
const readJson = async (filePath) => { const readJson = async (filePath) => {
try { try {
const content = await fs.readFile(filePath, "utf-8"); const content = await fs.readFile(filePath, "utf-8");
return JSON.parse(content); // Tolerate JSONC (trailing commas) and treat unparseable files as "no config"
// rather than throwing a 500 that the UI misreads as "tool not installed".
const stripped = content.replace(/,(\s*[}\]])/g, "$1");
return JSON.parse(stripped);
} catch (error) { } catch (error) {
if (error.code === "ENOENT") return null; return null;
throw error;
} }
}; };

View file

@ -21,10 +21,12 @@ const getConfigPath = () => {
const readConfig = async () => { const readConfig = async () => {
try { try {
const content = await fs.readFile(getConfigPath(), "utf-8"); const content = await fs.readFile(getConfigPath(), "utf-8");
return JSON.parse(content); // Tolerate JSONC (trailing commas) and treat unparseable files as "no config"
// rather than throwing a 500 that the UI misreads as "tool not installed".
const stripped = content.replace(/,(\s*[}\]])/g, "$1");
return JSON.parse(stripped);
} catch (error) { } catch (error) {
if (error.code === "ENOENT") return null; return null;
throw error;
} }
}; };

View file

@ -116,10 +116,14 @@ const get1pRoot = () => {
const get1pConfigPath = () => path.join(get1pRoot(), "claude_desktop_config.json"); const get1pConfigPath = () => path.join(get1pRoot(), "claude_desktop_config.json");
const read1pConfig = async () => { const read1pConfig = async () => {
try { return JSON.parse(await fs.readFile(get1pConfigPath(), "utf-8")) || {}; } try {
catch (error) { const content = await fs.readFile(get1pConfigPath(), "utf-8");
if (error.code === "ENOENT") return {}; // Tolerate JSONC (trailing commas) and treat unparseable files as empty config
throw error; // rather than throwing a 500 that the UI misreads as "tool not installed".
const stripped = content.replace(/,(\s*[}\]])/g, "$1");
return JSON.parse(stripped) || {};
} catch (error) {
return {};
} }
}; };
@ -193,10 +197,14 @@ const checkInstalled = async () => {
}; };
const readJson = async (filePath) => { const readJson = async (filePath) => {
try { return JSON.parse(await fs.readFile(filePath, "utf-8")); } try {
catch (error) { const content = await fs.readFile(filePath, "utf-8");
if (error.code === "ENOENT") return null; // Tolerate JSONC (trailing commas) and treat unparseable files as "no config"
throw error; // rather than throwing a 500 that the UI misreads as "tool not installed".
const stripped = content.replace(/,(\s*[}\]])/g, "$1");
return JSON.parse(stripped);
} catch (error) {
return null;
} }
}; };

View file

@ -37,10 +37,12 @@ const readSettings = async () => {
try { try {
const settingsPath = getDroidSettingsPath(); const settingsPath = getDroidSettingsPath();
const content = await fs.readFile(settingsPath, "utf-8"); const content = await fs.readFile(settingsPath, "utf-8");
return JSON.parse(content); // Tolerate JSONC (trailing commas) and treat unparseable files as "no config"
// rather than throwing a 500 that the UI misreads as "tool not installed".
const stripped = content.replace(/,(\s*[}\]])/g, "$1");
return JSON.parse(stripped);
} catch (error) { } catch (error) {
if (error.code === "ENOENT") return null; return null;
throw error;
} }
}; };

View file

@ -35,10 +35,12 @@ const checkInstalled = async () => {
const readJson = async (filePath) => { const readJson = async (filePath) => {
try { try {
const content = await fs.readFile(filePath, "utf-8"); const content = await fs.readFile(filePath, "utf-8");
return JSON.parse(content); // Tolerate JSONC (trailing commas) and treat unparseable files as "no config"
// rather than throwing a 500 that the UI misreads as "tool not installed".
const stripped = content.replace(/,(\s*[}\]])/g, "$1");
return JSON.parse(stripped);
} catch (error) { } catch (error) {
if (error.code === "ENOENT") return null; return null;
throw error;
} }
}; };

View file

@ -47,10 +47,12 @@ const readSettings = async () => {
try { try {
const settingsPath = getOpenClawSettingsPath(); const settingsPath = getOpenClawSettingsPath();
const content = await fs.readFile(settingsPath, "utf-8"); const content = await fs.readFile(settingsPath, "utf-8");
return JSON.parse(content); // Tolerate JSONC (trailing commas) and treat unparseable files as "no config"
// rather than throwing a 500 that the UI misreads as "tool not installed".
const stripped = content.replace(/,(\s*[}\]])/g, "$1");
return JSON.parse(stripped);
} catch (error) { } catch (error) {
if (error.code === "ENOENT") return null; return null;
throw error;
} }
}; };

View file

@ -35,10 +35,16 @@ const checkOpenCodeInstalled = async () => {
const readConfig = async () => { const readConfig = async () => {
try { try {
const content = await fs.readFile(getConfigPath(), "utf-8"); const content = await fs.readFile(getConfigPath(), "utf-8");
return JSON.parse(content); // opencode config files may use JSONC format (trailing commas, comments).
// Strip trailing commas before parsing to avoid SyntaxError on valid JSONC.
const stripped = content.replace(/,(\s*[}\]])/g, "$1");
return JSON.parse(stripped);
} catch (error) { } catch (error) {
if (error.code === "ENOENT") return null; if (error.code === "ENOENT") return null;
throw error; // If the config file exists but is unparseable (corrupted, exotic JSONC),
// treat it as "no config" rather than throwing a 500 that the UI
// misinterprets as "opencode not installed".
return null;
} }
}; };