Skip to content

Commit 84185c9

Browse files
committed
feat: support custom Claude CLI path and config directory
Add centralized configuration module (cli-config.ts) to support switching between claude and claude-internal (or any custom fork). Settings UI: - Claude CLI Path: custom executable path, auto-detect by default - Claude Config Directory: custom config dir, defaults to ~/.claude Changes: - New src/lib/cli-config.ts: centralized config with expandTilde(), getClaudeConfigDir(), getClaudeBinaryName(), and path helpers for commands/skills/projects/settings/plugins directories - Replace 15 files' hardcoded ~/.claude and binary references with cli-config.ts helpers - Backend validation on save: CLI path must be a file, config dir must be a directory; returns validation result to frontend - Frontend shows green "saved" or red "invalid" status with descriptive error messages - i18n support for en/zh on all new UI strings
1 parent 5413eb5 commit 84185c9

15 files changed

Lines changed: 2345 additions & 1554 deletions

File tree

src/app/api/plugins/[id]/route.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
1-
import { NextRequest, NextResponse } from 'next/server';
2-
import fs from 'fs';
3-
import path from 'path';
4-
import os from 'os';
5-
import type { PluginInfo, ErrorResponse, SuccessResponse } from '@/types';
1+
import { NextRequest, NextResponse } from "next/server";
2+
import fs from "fs";
3+
import path from "path";
4+
import os from "os";
5+
import type { PluginInfo, ErrorResponse, SuccessResponse } from "@/types";
66

77
function getClaudeDir(): string {
8-
return path.join(os.homedir(), '.claude');
8+
const { getClaudeConfigDir } = require("@/lib/cli-config");
9+
return getClaudeConfigDir();
910
}
1011

1112
function getSettingsPath(): string {
12-
return path.join(getClaudeDir(), 'settings.json');
13+
return path.join(getClaudeDir(), "settings.json");
1314
}
1415

1516
function readSettings(): Record<string, unknown> {
1617
const settingsPath = getSettingsPath();
1718
if (!fs.existsSync(settingsPath)) return {};
1819
try {
19-
return JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
20+
return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
2021
} catch {
2122
return {};
2223
}
@@ -28,28 +29,28 @@ function writeSettings(settings: Record<string, unknown>): void {
2829
if (!fs.existsSync(dir)) {
2930
fs.mkdirSync(dir, { recursive: true });
3031
}
31-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
32+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
3233
}
3334

3435
export async function GET(
3536
_request: NextRequest,
36-
{ params }: { params: Promise<{ id: string }> }
37+
{ params }: { params: Promise<{ id: string }> },
3738
): Promise<NextResponse<{ plugin: PluginInfo } | ErrorResponse>> {
3839
const { id } = await params;
3940
const pluginName = decodeURIComponent(id);
4041

4142
// Check in commands directory
42-
const commandsDir = path.join(getClaudeDir(), 'commands');
43+
const commandsDir = path.join(getClaudeDir(), "commands");
4344
const filePath = path.join(commandsDir, `${pluginName}.md`);
4445

4546
if (fs.existsSync(filePath)) {
46-
const content = fs.readFileSync(filePath, 'utf-8');
47-
const firstLine = content.split('\n')[0]?.trim() || '';
47+
const content = fs.readFileSync(filePath, "utf-8");
48+
const firstLine = content.split("\n")[0]?.trim() || "";
4849
return NextResponse.json({
4950
plugin: {
5051
name: pluginName,
51-
description: firstLine.startsWith('#')
52-
? firstLine.replace(/^#+\s*/, '')
52+
description: firstLine.startsWith("#")
53+
? firstLine.replace(/^#+\s*/, "")
5354
: `Skill: /${pluginName}`,
5455
enabled: true,
5556
},
@@ -73,12 +74,12 @@ export async function GET(
7374
});
7475
}
7576

76-
return NextResponse.json({ error: 'Plugin not found' }, { status: 404 });
77+
return NextResponse.json({ error: "Plugin not found" }, { status: 404 });
7778
}
7879

7980
export async function PUT(
8081
request: NextRequest,
81-
{ params }: { params: Promise<{ id: string }> }
82+
{ params }: { params: Promise<{ id: string }> },
8283
): Promise<NextResponse<SuccessResponse | ErrorResponse>> {
8384
try {
8485
const { id } = await params;
@@ -106,8 +107,11 @@ export async function PUT(
106107
return NextResponse.json({ success: true });
107108
} catch (error) {
108109
return NextResponse.json(
109-
{ error: error instanceof Error ? error.message : 'Failed to update plugin' },
110-
{ status: 500 }
110+
{
111+
error:
112+
error instanceof Error ? error.message : "Failed to update plugin",
113+
},
114+
{ status: 500 },
111115
);
112116
}
113117
}
Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import { NextRequest, NextResponse } from 'next/server';
2-
import fs from 'fs';
3-
import path from 'path';
4-
import os from 'os';
5-
import type { MCPServerConfig, ErrorResponse, SuccessResponse } from '@/types';
1+
import { NextRequest, NextResponse } from "next/server";
2+
import fs from "fs";
3+
import path from "path";
4+
import os from "os";
5+
import type { MCPServerConfig, ErrorResponse, SuccessResponse } from "@/types";
66

77
function getSettingsPath(): string {
8-
return path.join(os.homedir(), '.claude', 'settings.json');
8+
const { getClaudeSettingsPath } = require("@/lib/cli-config");
9+
return getClaudeSettingsPath();
910
}
1011

1112
function readSettings(): Record<string, unknown> {
1213
const settingsPath = getSettingsPath();
1314
if (!fs.existsSync(settingsPath)) return {};
1415
try {
15-
return JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
16+
return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
1617
} catch {
1718
return {};
1819
}
@@ -24,24 +25,27 @@ function writeSettings(settings: Record<string, unknown>): void {
2425
if (!fs.existsSync(dir)) {
2526
fs.mkdirSync(dir, { recursive: true });
2627
}
27-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
28+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
2829
}
2930

3031
export async function DELETE(
3132
_request: NextRequest,
32-
{ params }: { params: Promise<{ name: string }> }
33+
{ params }: { params: Promise<{ name: string }> },
3334
): Promise<NextResponse<SuccessResponse | ErrorResponse>> {
3435
try {
3536
const { name } = await params;
3637
const serverName = decodeURIComponent(name);
3738

3839
const settings = readSettings();
39-
const mcpServers = (settings.mcpServers || {}) as Record<string, MCPServerConfig>;
40+
const mcpServers = (settings.mcpServers || {}) as Record<
41+
string,
42+
MCPServerConfig
43+
>;
4044

4145
if (!mcpServers[serverName]) {
4246
return NextResponse.json(
4347
{ error: `MCP server "${serverName}" not found` },
44-
{ status: 404 }
48+
{ status: 404 },
4549
);
4650
}
4751

@@ -52,8 +56,13 @@ export async function DELETE(
5256
return NextResponse.json({ success: true });
5357
} catch (error) {
5458
return NextResponse.json(
55-
{ error: error instanceof Error ? error.message : 'Failed to delete MCP server' },
56-
{ status: 500 }
59+
{
60+
error:
61+
error instanceof Error
62+
? error.message
63+
: "Failed to delete MCP server",
64+
},
65+
{ status: 500 },
5766
);
5867
}
5968
}

src/app/api/plugins/mcp/route.ts

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
1-
import { NextRequest, NextResponse } from 'next/server';
2-
import fs from 'fs';
3-
import path from 'path';
4-
import os from 'os';
1+
import { NextRequest, NextResponse } from "next/server";
2+
import fs from "fs";
3+
import path from "path";
4+
import os from "os";
55
import type {
66
MCPServerConfig,
77
MCPConfigResponse,
88
ErrorResponse,
99
SuccessResponse,
10-
} from '@/types';
10+
} from "@/types";
1111

1212
function getSettingsPath(): string {
13-
return path.join(os.homedir(), '.claude', 'settings.json');
13+
const { getClaudeSettingsPath } = require("@/lib/cli-config");
14+
return getClaudeSettingsPath();
1415
}
1516

16-
// ~/.claude.json — Claude CLI stores user-scoped MCP servers here
17+
// User-level config file (e.g. ~/.claude.json or ~/.claude-internal.json)
1718
function getUserConfigPath(): string {
18-
return path.join(os.homedir(), '.claude.json');
19+
const { getClaudeUserConfigPath } = require("@/lib/cli-config");
20+
return getClaudeUserConfigPath();
1921
}
2022

2123
function readJsonFile(filePath: string): Record<string, unknown> {
2224
if (!fs.existsSync(filePath)) return {};
2325
try {
24-
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
26+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
2527
} catch {
2628
return {};
2729
}
@@ -37,10 +39,12 @@ function writeSettings(settings: Record<string, unknown>): void {
3739
if (!fs.existsSync(dir)) {
3840
fs.mkdirSync(dir, { recursive: true });
3941
}
40-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
42+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
4143
}
4244

43-
export async function GET(): Promise<NextResponse<MCPConfigResponse | ErrorResponse>> {
45+
export async function GET(): Promise<
46+
NextResponse<MCPConfigResponse | ErrorResponse>
47+
> {
4448
try {
4549
const settings = readSettings();
4650
const userConfig = readJsonFile(getUserConfigPath());
@@ -52,18 +56,23 @@ export async function GET(): Promise<NextResponse<MCPConfigResponse | ErrorRespo
5256
return NextResponse.json({ mcpServers });
5357
} catch (error) {
5458
return NextResponse.json(
55-
{ error: error instanceof Error ? error.message : 'Failed to read MCP config' },
56-
{ status: 500 }
59+
{
60+
error:
61+
error instanceof Error ? error.message : "Failed to read MCP config",
62+
},
63+
{ status: 500 },
5764
);
5865
}
5966
}
6067

6168
export async function PUT(
62-
request: NextRequest
69+
request: NextRequest,
6370
): Promise<NextResponse<SuccessResponse | ErrorResponse>> {
6471
try {
6572
const body = await request.json();
66-
const { mcpServers } = body as { mcpServers: Record<string, MCPServerConfig> };
73+
const { mcpServers } = body as {
74+
mcpServers: Record<string, MCPServerConfig>;
75+
};
6776

6877
const settings = readSettings();
6978
settings.mcpServers = mcpServers;
@@ -72,23 +81,28 @@ export async function PUT(
7281
return NextResponse.json({ success: true });
7382
} catch (error) {
7483
return NextResponse.json(
75-
{ error: error instanceof Error ? error.message : 'Failed to update MCP config' },
76-
{ status: 500 }
84+
{
85+
error:
86+
error instanceof Error
87+
? error.message
88+
: "Failed to update MCP config",
89+
},
90+
{ status: 500 },
7791
);
7892
}
7993
}
8094

8195
export async function POST(
82-
request: NextRequest
96+
request: NextRequest,
8397
): Promise<NextResponse<SuccessResponse | ErrorResponse>> {
8498
try {
8599
const body = await request.json();
86100
const { name, server } = body as { name: string; server: MCPServerConfig };
87101

88102
if (!name || !server || !server.command) {
89103
return NextResponse.json(
90-
{ error: 'Name and server command are required' },
91-
{ status: 400 }
104+
{ error: "Name and server command are required" },
105+
{ status: 400 },
92106
);
93107
}
94108

@@ -101,7 +115,7 @@ export async function POST(
101115
if (mcpServers[name]) {
102116
return NextResponse.json(
103117
{ error: `MCP server "${name}" already exists` },
104-
{ status: 409 }
118+
{ status: 409 },
105119
);
106120
}
107121

@@ -111,8 +125,11 @@ export async function POST(
111125
return NextResponse.json({ success: true });
112126
} catch (error) {
113127
return NextResponse.json(
114-
{ error: error instanceof Error ? error.message : 'Failed to add MCP server' },
115-
{ status: 500 }
128+
{
129+
error:
130+
error instanceof Error ? error.message : "Failed to add MCP server",
131+
},
132+
{ status: 500 },
116133
);
117134
}
118135
}

0 commit comments

Comments
 (0)