Skip to content

Commit 033644f

Browse files
committed
Add env manager to dashboard
1 parent d64b658 commit 033644f

8 files changed

Lines changed: 355 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 2.0.4
4+
- UI üzerinden `.env` anahtarları ekleme/güncelleme eklendi.
5+
- API tarafına `GET/POST /api/env` eklendi.
6+
37
## 2.0.3
48
- API anahtarları için `~/.env` otomatik yükleme eklendi (CLI + health monitor).
59
- Sağlayıcı API key çözümleme tek noktaya alındı.

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# Claude Code Router Config - Advanced Multi-Provider Setup
22

3-
🚀 **v2.0.3** - Unified router + config package with z.ai (GLM 4.7) support, advanced CLI tools, analytics, smart routing, and configuration templates!
3+
🚀 **v2.0.4** - Unified router + config package with z.ai (GLM 4.7) support, advanced CLI tools, analytics, smart routing, and configuration templates!
44

55
Use Claude Code as a single interface to access multiple AI providers with intelligent routing for optimal performance, cost, and quality.
66

7-
## ✨ New in v2.0.3
7+
## ✨ New in v2.0.4
8+
- UI üzerinden `.env` anahtarları ekleme/güncelleme (TR/NL).
89
- `~/.env` otomatik yükleme ile API anahtarlarının bulunması (CLI + health monitor).
910
- **z.ai Support**: Native integration for GLM-4.7 via z.ai (PPInfra).
1011
- **Lightweight Mode**: New `ccc` function for zero-dependency routing.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@halilertekin/claude-code-router-config",
3-
"version": "2.0.3",
3+
"version": "2.0.4",
44
"description": "Multi-provider configuration for Claude Code Router with intent-based routing, advanced CLI tools, analytics, and smart routing. Setup OpenAI, Anthropic, Gemini, Qwen, GLM, OpenRouter, and GitHub Copilot with intelligent routing.",
55
"main": "install.js",
66
"bin": {

router/config.js

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,15 @@ function parseEnvLine(line) {
3131
return { key, value };
3232
}
3333

34+
function getEnvPath() {
35+
return process.env.CCR_ENV_PATH || DEFAULT_ENV_PATH;
36+
}
37+
3438
function loadDotenv() {
3539
if (envLoaded) return;
3640
envLoaded = true;
3741

38-
const envPath = process.env.CCR_ENV_PATH || DEFAULT_ENV_PATH;
42+
const envPath = getEnvPath();
3943
if (!fs.existsSync(envPath)) return;
4044

4145
const content = fs.readFileSync(envPath, 'utf8');
@@ -62,6 +66,73 @@ function resolveEnv(value) {
6266
});
6367
}
6468

69+
function readEnvFile() {
70+
const envPath = getEnvPath();
71+
if (!fs.existsSync(envPath)) {
72+
return { path: envPath, entries: {} };
73+
}
74+
75+
const content = fs.readFileSync(envPath, 'utf8');
76+
const entries = {};
77+
content.split(/\r?\n/).forEach((line) => {
78+
const parsed = parseEnvLine(line);
79+
if (!parsed) return;
80+
entries[parsed.key] = parsed.value;
81+
});
82+
83+
return { path: envPath, entries };
84+
}
85+
86+
function formatEnvValue(value) {
87+
const safe = /^[A-Za-z0-9_./:@-]+$/.test(value);
88+
if (safe) return value;
89+
const escaped = value
90+
.replace(/\\/g, '\\\\')
91+
.replace(/"/g, '\\"')
92+
.replace(/\n/g, '\\n');
93+
return `"${escaped}"`;
94+
}
95+
96+
function writeEnvValue(key, value) {
97+
const envPath = getEnvPath();
98+
const normalizedKey = String(key || '').trim();
99+
if (!/^[A-Za-z0-9_]+$/.test(normalizedKey)) {
100+
throw new Error('Invalid environment key');
101+
}
102+
103+
const normalizedValue = value === undefined || value === null ? '' : String(value);
104+
const formattedValue = formatEnvValue(normalizedValue);
105+
106+
let lines = [];
107+
let updated = false;
108+
109+
if (fs.existsSync(envPath)) {
110+
const content = fs.readFileSync(envPath, 'utf8');
111+
lines = content.split(/\r?\n/);
112+
}
113+
114+
lines = lines.map((line) => {
115+
const parsed = parseEnvLine(line);
116+
if (!parsed || parsed.key !== normalizedKey) return line;
117+
const prefix = line.trim().startsWith('export ') ? 'export ' : '';
118+
updated = true;
119+
return `${prefix}${normalizedKey}=${formattedValue}`;
120+
});
121+
122+
if (!updated) {
123+
lines.push(`${normalizedKey}=${formattedValue}`);
124+
}
125+
126+
const output = lines.filter((line, index, arr) => {
127+
if (index === arr.length - 1) return true;
128+
return line !== '' || arr[index + 1] !== '';
129+
});
130+
131+
fs.writeFileSync(envPath, `${output.join('\n')}\n`, 'utf8');
132+
133+
return { path: envPath, updated: true };
134+
}
135+
65136
function resolveConfigValue(value, key) {
66137
if (Array.isArray(value)) {
67138
return value.map((item) => resolveConfigValue(item, key));
@@ -125,6 +196,9 @@ function resolveProviderKey(provider) {
125196

126197
module.exports = {
127198
loadConfig,
199+
getEnvPath,
200+
readEnvFile,
201+
writeEnvValue,
128202
getConfigPath,
129203
getConfigDir,
130204
resolveProviderKey

router/server.js

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ const fs = require('fs');
66
const os = require('os');
77
const path = require('path');
88

9-
const { loadConfig, getConfigPath, getConfigDir } = require('./config');
9+
const {
10+
loadConfig,
11+
getConfigPath,
12+
getConfigDir,
13+
readEnvFile,
14+
writeEnvValue
15+
} = require('./config');
1016
const { resolveRoute, estimateTokens } = require('./route');
1117
const {
1218
anthropicToOpenAI,
@@ -299,6 +305,59 @@ function setupApi(app) {
299305
}
300306
});
301307

308+
app.get('/api/env', (req, res) => {
309+
try {
310+
const config = loadConfig();
311+
const envFile = readEnvFile();
312+
const keys = new Set();
313+
314+
(config.Providers || []).forEach((provider) => {
315+
if (typeof provider.api_key === 'string' && provider.api_key.startsWith('$')) {
316+
keys.add(provider.api_key.slice(1));
317+
}
318+
});
319+
320+
if ((config.Providers || []).some((provider) => provider.name?.toLowerCase() === 'openrouter')) {
321+
keys.add('OPENROUTER_REFERRER');
322+
keys.add('OPENROUTER_APP_NAME');
323+
}
324+
325+
const data = Array.from(keys)
326+
.sort()
327+
.map((key) => {
328+
const value = process.env[key] || envFile.entries[key] || '';
329+
return { name: key, present: Boolean(value) };
330+
});
331+
332+
res.json({ success: true, data: { envPath: envFile.path, keys: data } });
333+
} catch (error) {
334+
res.status(500).json({ success: false, error: error.message });
335+
}
336+
});
337+
338+
app.post('/api/env', (req, res) => {
339+
try {
340+
const key = String(req.body?.key || '').trim();
341+
const value = req.body?.value;
342+
343+
if (!key) {
344+
res.status(400).json({ success: false, error: 'Missing env key' });
345+
return;
346+
}
347+
348+
if (value === undefined || value === null || String(value).trim() === '') {
349+
res.status(400).json({ success: false, error: 'Missing env value' });
350+
return;
351+
}
352+
353+
const result = writeEnvValue(key, value);
354+
process.env[key] = String(value);
355+
res.json({ success: true, data: { key, path: result.path } });
356+
} catch (error) {
357+
res.status(500).json({ success: false, error: error.message });
358+
}
359+
});
360+
302361
app.post('/api/config', (req, res) => {
303362
try {
304363
const configPath = getConfigPath();

web-dashboard/public/css/dashboard.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,40 @@ body::before {
193193
gap: 10px;
194194
}
195195

196+
.form {
197+
display: grid;
198+
gap: 12px;
199+
}
200+
201+
.form-label {
202+
font-size: 13px;
203+
text-transform: uppercase;
204+
letter-spacing: 0.08em;
205+
color: var(--muted);
206+
}
207+
208+
.field-row {
209+
display: flex;
210+
gap: 10px;
211+
flex-wrap: wrap;
212+
}
213+
196214
.select {
197215
border: 1px solid var(--border);
198216
border-radius: 10px;
199217
padding: 8px 12px;
200218
background: #fff;
219+
min-width: 160px;
220+
}
221+
222+
.input {
223+
border: 1px solid var(--border);
224+
border-radius: 10px;
225+
padding: 10px 12px;
226+
background: #fff;
227+
flex: 1 1 180px;
228+
min-width: 160px;
229+
font-family: inherit;
201230
}
202231

203232
.grid {

web-dashboard/public/index.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,42 @@ <h3 data-i18n="configJson">Konfigürasyon</h3>
185185
</div>
186186
</div>
187187
</section>
188+
189+
<section class="panel">
190+
<div class="panel-header">
191+
<h2 data-i18n="env">Ortam Değişkenleri</h2>
192+
<span class="muted" data-i18n="envHint">Anahtarları hızlıca güncelle</span>
193+
</div>
194+
<div class="grid two">
195+
<div class="card">
196+
<div class="card-header">
197+
<h3 data-i18n="envStatus">Durum</h3>
198+
</div>
199+
<div class="card-body">
200+
<div id="env-list" class="list"></div>
201+
<div id="env-path" class="muted"></div>
202+
</div>
203+
</div>
204+
<div class="card">
205+
<div class="card-header">
206+
<h3 data-i18n="envUpdate">Güncelle</h3>
207+
</div>
208+
<div class="card-body">
209+
<div class="form">
210+
<label class="form-label" data-i18n="envKeyLabel">Anahtar</label>
211+
<div class="field-row">
212+
<select id="env-key-select" class="select"></select>
213+
<input id="env-key-custom" class="input" data-i18n-placeholder="envKeyPlaceholder" placeholder="CUSTOM_KEY">
214+
</div>
215+
<label class="form-label" data-i18n="envValueLabel">Değer</label>
216+
<input id="env-value" class="input" type="password" placeholder="********">
217+
<button class="btn" id="env-save" data-i18n="envSave">Kaydet</button>
218+
<span id="env-result" class="muted"></span>
219+
</div>
220+
</div>
221+
</div>
222+
</div>
223+
</section>
188224
</main>
189225

190226
<footer class="footer">

0 commit comments

Comments
 (0)