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 {
const settingsPath = getClaudeSettingsPath();
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) {
if (error.code === "ENOENT") {
return null;
}
throw error;
return null;
}
};

View File

@@ -35,10 +35,12 @@ const checkInstalled = async () => {
const readJson = async (filePath) => {
try {
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) {
if (error.code === "ENOENT") return null;
throw error;
return null;
}
};

View File

@@ -21,10 +21,12 @@ const getConfigPath = () => {
const readConfig = async () => {
try {
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) {
if (error.code === "ENOENT") return null;
throw error;
return null;
}
};

View File

@@ -116,10 +116,14 @@ const get1pRoot = () => {
const get1pConfigPath = () => path.join(get1pRoot(), "claude_desktop_config.json");
const read1pConfig = async () => {
try { return JSON.parse(await fs.readFile(get1pConfigPath(), "utf-8")) || {}; }
catch (error) {
if (error.code === "ENOENT") return {};
throw error;
try {
const content = await fs.readFile(get1pConfigPath(), "utf-8");
// Tolerate JSONC (trailing commas) and treat unparseable files as empty 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) {
return {};
}
};
@@ -193,10 +197,14 @@ const checkInstalled = async () => {
};
const readJson = async (filePath) => {
try { return JSON.parse(await fs.readFile(filePath, "utf-8")); }
catch (error) {
if (error.code === "ENOENT") return null;
throw error;
try {
const content = await fs.readFile(filePath, "utf-8");
// 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) {
return null;
}
};

View File

@@ -37,10 +37,12 @@ const readSettings = async () => {
try {
const settingsPath = getDroidSettingsPath();
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) {
if (error.code === "ENOENT") return null;
throw error;
return null;
}
};

View File

@@ -35,10 +35,12 @@ const checkInstalled = async () => {
const readJson = async (filePath) => {
try {
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) {
if (error.code === "ENOENT") return null;
throw error;
return null;
}
};

View File

@@ -47,10 +47,12 @@ const readSettings = async () => {
try {
const settingsPath = getOpenClawSettingsPath();
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) {
if (error.code === "ENOENT") return null;
throw error;
return null;
}
};

View File

@@ -35,10 +35,16 @@ const checkOpenCodeInstalled = async () => {
const readConfig = async () => {
try {
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) {
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;
}
};