Skip to content

Commit 6a86643

Browse files
committed
feat: replace json5 with comment-json to preserve comments
1 parent 9f149cf commit 6a86643

4 files changed

Lines changed: 124 additions & 1470 deletions

File tree

packages/cli/commands/mcp.ts

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { confirm, select } from "@inquirer/prompts";
1+
import { select } from "@inquirer/prompts";
22
import chalk from "chalk";
33
import { Command } from "commander";
4-
import JSON5 from "json5";
4+
import { parse as parseJSON, stringify as stringifyJSON } from "comment-json";
55
import { mkdir, readFile, writeFile } from "node:fs/promises";
66
import { dirname, join, relative } from "node:path";
77
import ora, { type Ora } from "ora";
@@ -117,50 +117,31 @@ export const mcpAction = async (options: {
117117
`Reading configuration file: ${chalk.cyan(displayConfigPath)}...`,
118118
).start();
119119
let editorConfig: any = {};
120-
try {
121-
if (await fileExists(configPath)) {
122-
const content = await readFile(configPath, "utf-8");
123-
// Attempt to parse JSON, handle potential comments if needed
124-
try {
125-
editorConfig = JSON5.parse(content);
126-
} catch (parseError) {
127-
spinner.fail(
128-
`Failed to parse configuration file ${chalk.cyan(displayConfigPath)}.`,
129-
);
130-
throw parseError; // Re-throw original error if cleaning fails
131-
}
132-
spinner.succeed("Configuration file read.");
133-
} else {
134-
spinner.info("Configuration file not found, will create a new one.");
135-
editorConfig = {}; // Initialize empty config if file doesn't exist
136-
}
137-
} catch (error: any) {
138-
// Handle file access errors separately from parsing errors
139-
if (error.code !== "ENOENT") {
140-
// ENOENT means file not found, which is handled above
141-
spinner.fail("Failed to read configuration file.");
142-
console.error(
143-
chalk.red(`Error reading ${displayConfigPath}: ${error.message}`),
120+
if (await fileExists(configPath)) {
121+
const content = await readFile(configPath, "utf-8");
122+
// Attempt to parse JSON, handle potential comments if needed
123+
try {
124+
editorConfig = parseJSON(content);
125+
} catch {
126+
spinner.fail(
127+
`Failed to parse configuration file ${chalk.cyan(displayConfigPath)}.`,
144128
);
145-
const proceed = await confirm({
146-
message: "Reading failed. Attempt to overwrite the file?",
147-
default: false,
148-
});
149-
if (!proceed) return;
150-
editorConfig = {}; // Start fresh if reading failed and user agreed
151-
} else {
152-
// This case should ideally be caught by the fileExists check, but handle defensively
153-
spinner.info("Configuration file not found, creating a new one.");
154-
editorConfig = {};
155129
}
130+
spinner.succeed(
131+
`Read configuration file ${chalk.cyan(displayConfigPath)}.`,
132+
);
133+
} else {
134+
spinner.info("Configuration file not found, will create a new one.");
135+
editorConfig = {}; // Initialize empty config if file doesn't exist
156136
}
157137

158-
// Ensure mcpServers object exists and get existing Bucket entries
138+
// Ensure MCP servers object exists
159139
const serversConfig = getServersConfig(
160140
editorConfig,
161141
selectedEditor,
162142
configPathType,
163143
);
144+
// Check for existing Bucket servers
164145
const existingBucketEntries = Object.keys(serversConfig).filter((key) =>
165146
/bucket/i.test(key),
166147
);
@@ -203,10 +184,10 @@ export const mcpAction = async (options: {
203184

204185
const newEntryValue = {
205186
type: "stdio",
206-
command: "npx", // Assuming npx is standard
187+
command: "npx",
207188
args: [
208-
"mcp-remote@next", // Use the specified package
209-
mcpUrlWithAppId, // Always include appId explicitly
189+
"mcp-remote@next", // todo: remove next once stable
190+
mcpUrlWithAppId, // Always include appId explicitly as that is more stable
210191
],
211192
};
212193

@@ -220,7 +201,7 @@ export const mcpAction = async (options: {
220201
try {
221202
// Ensure the directory exists before writing
222203
await mkdir(dirname(configPath), { recursive: true });
223-
const configString = JSON.stringify(editorConfig, null, 2); // Pretty print JSON
204+
const configString = stringifyJSON(editorConfig, null, 2);
224205
await writeFile(configPath, configString);
225206
spinner.succeed(
226207
`Configuration updated successfully in ${chalk.cyan(displayConfigPath)}.`,
@@ -231,7 +212,9 @@ export const mcpAction = async (options: {
231212
),
232213
);
233214
} catch (error) {
234-
spinner.fail("Failed to write configuration file.");
215+
spinner.fail(
216+
`Failed to write configuration file ${chalk.cyan(displayConfigPath)}.`,
217+
);
235218
void handleError(error, "MCP Configuration");
236219
}
237220
};

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@
4646
"chalk": "^5.3.0",
4747
"change-case": "^5.4.4",
4848
"commander": "^12.1.0",
49+
"comment-json": "^4.2.5",
4950
"express": "^4.21.2",
5051
"fast-deep-equal": "^3.1.3",
5152
"find-up": "^7.0.0",
52-
"json5": "^2.2.3",
5353
"open": "^10.1.0",
5454
"ora": "^8.1.0",
5555
"slug": "^10.0.0",

packages/cli/stores/config.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Ajv, ValidateFunction } from "ajv";
2+
import {
3+
assign as assignJSON,
4+
parse as parseJSON,
5+
stringify as stringifyJSON,
6+
} from "comment-json";
27
import equal from "fast-deep-equal";
38
import { findUp } from "find-up";
4-
import JSON5 from "json5";
59
import { readFile, writeFile } from "node:fs/promises";
610
import { dirname, join } from "node:path";
711
import { fileURLToPath } from "node:url";
@@ -71,7 +75,7 @@ class ConfigStore {
7175
"schema.json",
7276
);
7377
const content = await readFile(schemaPath, "utf-8");
74-
const parsed = JSON5.parse<Config>(content);
78+
const parsed = parseJSON(content) as unknown as Config;
7579
const ajv = new Ajv();
7680
this.validateConfig = ajv.compile(parsed);
7781
} catch {
@@ -95,7 +99,7 @@ class ConfigStore {
9599
if (!this.configPath) return;
96100

97101
const content = await readFile(this.configPath, "utf-8");
98-
const parsed = JSON5.parse<Partial<Config>>(content);
102+
const parsed = parseJSON(content) as unknown as Partial<Config>;
99103

100104
// Normalize values
101105
if (parsed.baseUrl)
@@ -112,7 +116,7 @@ class ConfigStore {
112116
);
113117
}
114118

115-
this.config = { ...this.config, ...parsed };
119+
this.config = assignJSON(this.config, parsed);
116120
} catch {
117121
// No config file found
118122
}
@@ -123,9 +127,7 @@ class ConfigStore {
123127
* @param overwrite If true, overwrites existing config file. Defaults to false
124128
*/
125129
async saveConfigFile(overwrite = false) {
126-
const configWithoutDefaults: Partial<Config> = {
127-
...this.config,
128-
};
130+
const configWithoutDefaults: Partial<Config> = assignJSON({}, this.config);
129131

130132
// Only include non-default values and $schema
131133
for (const untypedKey in configWithoutDefaults) {
@@ -138,7 +140,7 @@ class ConfigStore {
138140
}
139141
}
140142

141-
const configJSON = JSON.stringify(configWithoutDefaults, null, 2);
143+
const configJSON = stringifyJSON(configWithoutDefaults, null, 2);
142144

143145
if (this.configPath && !overwrite) {
144146
throw new Error("Config file already exists");

0 commit comments

Comments
 (0)