lcluster 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bots/template.js +79 -0
- package/eslint.config.js +24 -0
- package/package.json +4 -3
- package/src/bot/manager.js +46 -0
- package/src/cli.js +17 -0
- package/src/gateway/v4/rest.js +2 -0
- package/src/main.js +40 -1
- package/src/system/detect.js +1 -0
- package/src/tui/components/NodeCard.jsx +3 -1
- package/src/tui/init/ThemePicker.jsx +2 -1
- package/src/tui/screens/Dashboard.jsx +1 -1
- package/src/tui/screens/Settings.jsx +26 -4
- package/src/tui/theme/index.js +17 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<p>A powerful Lavalink cluster manager for your terminal.</p>
|
|
5
5
|
|
|
6
6
|
<p>
|
|
7
|
-
<img src="https://img.shields.io/badge/version-1.0.
|
|
7
|
+
<img src="https://img.shields.io/badge/version-1.0.2-blue" />
|
|
8
8
|
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-green" />
|
|
9
9
|
<img src="https://img.shields.io/badge/license-GPLv3-purple" />
|
|
10
10
|
<img src="https://img.shields.io/badge/built%20with-Claude%20AI-orange" />
|
|
@@ -111,7 +111,7 @@ Lavalink v4 compatible client.
|
|
|
111
111
|
|
|
112
112
|
```
|
|
113
113
|
╔═════════════════════════════════════════════════════════════════════╗
|
|
114
|
-
║ ⬡ lcluster v1.0.
|
|
114
|
+
║ ⬡ lcluster v1.0.2 ● 2 online ⚠ 1 warn gateway :2333 ● ║
|
|
115
115
|
╚═════════════════════════════════════════════════════════════════════╝
|
|
116
116
|
|
|
117
117
|
┌─ nodes (3/5) ──────────────────────────────────── [↑↓ scroll] ─┐
|
package/bots/template.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Client, GatewayIntentBits } from 'discord.js';
|
|
2
|
+
import { LavalinkManager } from 'lavalink-client';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
|
|
8
|
+
// 1. Token passed from lcluster bot manager securely
|
|
9
|
+
const DISCORD_TOKEN = process.env.DISCORD_TOKEN || 'YOUR_DISCORD_BOT_TOKEN';
|
|
10
|
+
|
|
11
|
+
// 2. Read lcluster internal port/password config directly to avoid desync
|
|
12
|
+
const configPath = path.join(os.homedir(), '.lcluster', 'config.yml');
|
|
13
|
+
let lclusterPassword = 'youshallnotpass';
|
|
14
|
+
let lclusterPort = 2333;
|
|
15
|
+
if (fs.existsSync(configPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const config = yaml.load(fs.readFileSync(configPath, 'utf8')) || {};
|
|
18
|
+
lclusterPassword = config.gateway?.password || 'youshallnotpass';
|
|
19
|
+
lclusterPort = config.gateway?.port || 2333;
|
|
20
|
+
} catch { }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LCLUSTER_NODE = {
|
|
24
|
+
id: "lcluster-gateway",
|
|
25
|
+
host: "localhost", // or your VPS IP
|
|
26
|
+
port: lclusterPort, // Dynamically synced from config
|
|
27
|
+
password: lclusterPassword, // Dynamically synced from config
|
|
28
|
+
secure: false // Set to true if behind HTTPS/WSS
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const client = new Client({
|
|
32
|
+
intents: [
|
|
33
|
+
GatewayIntentBits.Guilds,
|
|
34
|
+
GatewayIntentBits.GuildMessages,
|
|
35
|
+
GatewayIntentBits.GuildVoiceStates,
|
|
36
|
+
GatewayIntentBits.MessageContent
|
|
37
|
+
]
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Initialize lavalink-client manager pointing exactly to lcluster
|
|
41
|
+
const lavalink = new LavalinkManager({
|
|
42
|
+
nodes: [LCLUSTER_NODE],
|
|
43
|
+
sendToShard: (guildId, payload) => {
|
|
44
|
+
client.guilds.cache.get(guildId)?.shard?.send(payload);
|
|
45
|
+
},
|
|
46
|
+
client: {
|
|
47
|
+
id: "CLIENT_ID_PLACEHOLDER", // Replaced on login
|
|
48
|
+
username: "Lcluster Bot"
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
lavalink.nodeManager.on("connect", (node) => {
|
|
53
|
+
console.log(`[Lcluster] Successfully connected to gateway node: ${node.id}`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
lavalink.nodeManager.on("error", (node, error) => {
|
|
57
|
+
console.error(`[Lcluster] Error on node ${node.id}:`, error.message);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
client.on('ready', () => {
|
|
61
|
+
console.log(`[Discord] Logged in as ${client.user.tag}`);
|
|
62
|
+
lavalink.client.id = client.user.id;
|
|
63
|
+
lavalink.init({ ...client.user });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
client.on('messageCreate', async (message) => {
|
|
67
|
+
if (message.author.bot || !message.guild) return;
|
|
68
|
+
|
|
69
|
+
// Simple ping command
|
|
70
|
+
if (message.content === '!ping') {
|
|
71
|
+
message.reply('Pong! Cluster is active.');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Forward raw discord voice events to lavalink
|
|
76
|
+
client.on("raw", (d) => lavalink.sendRawData(d));
|
|
77
|
+
|
|
78
|
+
console.log("Starting Lcluster Discord Bot...");
|
|
79
|
+
client.login(DISCORD_TOKEN);
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
js.configs.recommended,
|
|
5
|
+
{
|
|
6
|
+
languageOptions: {
|
|
7
|
+
ecmaVersion: 2024,
|
|
8
|
+
sourceType: "module",
|
|
9
|
+
globals: {
|
|
10
|
+
process: "readonly",
|
|
11
|
+
console: "readonly",
|
|
12
|
+
__dirname: "readonly",
|
|
13
|
+
setTimeout: "readonly",
|
|
14
|
+
setInterval: "readonly",
|
|
15
|
+
clearInterval: "readonly"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
rules: {
|
|
19
|
+
"no-unused-vars": "warn",
|
|
20
|
+
"no-undef": "warn",
|
|
21
|
+
"no-empty": "warn"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lcluster",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A powerful Lavalink cluster manager for your terminal",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"type": "module",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"@babel/core": "^7.24.0",
|
|
48
48
|
"@babel/preset-env": "^7.24.0",
|
|
49
49
|
"@babel/preset-react": "^7.23.3",
|
|
50
|
-
"@babel/register": "^7.23.7"
|
|
50
|
+
"@babel/register": "^7.23.7",
|
|
51
|
+
"@eslint/js": "^10.0.1"
|
|
51
52
|
}
|
|
52
|
-
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { fork } from 'node:child_process';
|
|
5
|
+
import { events } from '../core/events.js';
|
|
6
|
+
|
|
7
|
+
let botProcess = null;
|
|
8
|
+
|
|
9
|
+
export async function startBot() {
|
|
10
|
+
const configPath = path.join(os.homedir(), '.lcluster', 'config.yml');
|
|
11
|
+
|
|
12
|
+
let config = {};
|
|
13
|
+
if (fs.existsSync(configPath)) {
|
|
14
|
+
try {
|
|
15
|
+
const yaml = await import('js-yaml');
|
|
16
|
+
config = yaml.default.load(fs.readFileSync(configPath, 'utf8')) || {};
|
|
17
|
+
} catch { }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (config?.bot?.enabled && config?.bot?.token) {
|
|
21
|
+
// Resolve absolute path to bots/template.js from local installation or global
|
|
22
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
23
|
+
const templatePath = path.join(__dirname, '..', '..', 'bots', 'template.js');
|
|
24
|
+
|
|
25
|
+
if (fs.existsSync(templatePath)) {
|
|
26
|
+
botProcess = fork(templatePath, [], {
|
|
27
|
+
env: { ...process.env, DISCORD_TOKEN: config.bot.token },
|
|
28
|
+
stdio: 'ignore' // We don't want the bot destroying our TUI with console.log
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
botProcess.on('error', (err) => {
|
|
32
|
+
events.emit('system:log', `Failed to start Custom Discord Bot: ${err.message}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
botProcess.on('exit', (code) => {
|
|
36
|
+
events.emit('system:log', `Custom Discord Bot naturally exited with code ${code}`);
|
|
37
|
+
botProcess = null;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Assume started successfully if we made it here
|
|
41
|
+
events.emit('system:log', 'Custom Discord Bot successfully mounted and detached to background.');
|
|
42
|
+
} else {
|
|
43
|
+
events.emit('system:log', 'Custom Discord Bot enabled but bots/template.js is missing.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -21,5 +21,22 @@ if (!process.env.LCLUSTER_TSX_BOOTSTRAPPED) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// Now we can safely import everything else because tsx loader is active
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
27
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
28
|
+
|
|
29
|
+
// Beta Version Lockdown (Phase 10)
|
|
30
|
+
if (pkg.version.includes('-beta')) {
|
|
31
|
+
// Determine if running from global node_modules by checking the path structure
|
|
32
|
+
if (pkgPath.includes('node_modules') || process.execPath === process.argv[1]) {
|
|
33
|
+
console.error('\n[Lcluster Error] Beta versions (v1.0.2-beta) cannot be run globally.\n');
|
|
34
|
+
console.error('Please clone the repository and run locally via:');
|
|
35
|
+
console.error(' git clone https://github.com/ramkrishna-js/lcluster.git');
|
|
36
|
+
console.error(' cd lcluster && npm install && npm link\n');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
const { runCLI } = await import('./main.js');
|
|
25
42
|
await runCLI();
|
package/src/gateway/v4/rest.js
CHANGED
|
@@ -12,6 +12,8 @@ export function handleRest(req, res) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// Handle specific session
|
|
15
|
+
// eslint-disable-next-line no-useless-escape
|
|
16
|
+
const isSessionUpdate = req.method === 'PATCH' && /^\/v4\/sessions\/[a-zA-Z0-9]+$/.test(req.url);
|
|
15
17
|
const sessionMatch = path.match(/^\/v4\/sessions\/([^\/]+)(.*)$/);
|
|
16
18
|
if (sessionMatch) {
|
|
17
19
|
const sessionId = sessionMatch[1];
|
package/src/main.js
CHANGED
|
@@ -16,7 +16,7 @@ export async function runCLI() {
|
|
|
16
16
|
.name('lcluster')
|
|
17
17
|
.description('⬡ lcluster — Lavalink Cluster Manager')
|
|
18
18
|
.version(`
|
|
19
|
-
⬡ lcluster v1.0.
|
|
19
|
+
⬡ lcluster v1.0.2
|
|
20
20
|
|
|
21
21
|
Built by Ram Krishna & Claude (Anthropic AI)
|
|
22
22
|
Lavalink Cluster Manager for Node.js
|
|
@@ -129,5 +129,44 @@ export async function runCLI() {
|
|
|
129
129
|
console.log(`Tailing logs for ${name}...`);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
program
|
|
133
|
+
.command('theme')
|
|
134
|
+
.description('generates a custom.json theme baseline')
|
|
135
|
+
.action(async () => {
|
|
136
|
+
const fs = await import('node:fs');
|
|
137
|
+
const path = await import('node:path');
|
|
138
|
+
const os = await import('node:os');
|
|
139
|
+
const customPath = path.join(os.homedir(), '.lcluster', 'custom.json');
|
|
140
|
+
|
|
141
|
+
const boilerplate = {
|
|
142
|
+
"border": "#00ff9f",
|
|
143
|
+
"borderDim": "#00ff9f33",
|
|
144
|
+
"text": "#00ff9f",
|
|
145
|
+
"textDim": "#00ff9f66",
|
|
146
|
+
"background": "#080b14",
|
|
147
|
+
"selected": "#00ff9f11",
|
|
148
|
+
"selectedBorder": "#00ff9f55",
|
|
149
|
+
"online": "#00ff9f",
|
|
150
|
+
"degraded": "#ffcc00",
|
|
151
|
+
"offline": "#ff3366",
|
|
152
|
+
"docker": "#4facfe",
|
|
153
|
+
"process": "#a8edea",
|
|
154
|
+
"accent": "#00ff9f",
|
|
155
|
+
"keyHint": "#00ff9f33"
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (fs.existsSync(customPath)) {
|
|
159
|
+
console.log(chalk.yellow(`\n⚠ A custom theme already exists at: ${customPath}`));
|
|
160
|
+
console.log('Modify the file directly to continue customizing your TUI!');
|
|
161
|
+
} else {
|
|
162
|
+
fs.mkdirSync(path.join(os.homedir(), '.lcluster'), { recursive: true });
|
|
163
|
+
fs.writeFileSync(customPath, JSON.stringify(boilerplate, null, 4), 'utf8');
|
|
164
|
+
console.log(chalk.green(`\n✔ Custom Theme built successfully at: ${customPath}`));
|
|
165
|
+
console.log(chalk.cyan('\n1. Edit the hex colors inside custom.json'));
|
|
166
|
+
console.log(chalk.cyan('2. Open the TUI Dashboard (`lcluster`)'));
|
|
167
|
+
console.log(chalk.cyan('3. Press [q] then Settings and select the "< custom >" theme limit!\n'));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
132
171
|
program.parse();
|
|
133
172
|
}
|
package/src/system/detect.js
CHANGED
|
@@ -49,7 +49,9 @@ export default function NodeCard({ node, isSelected }) {
|
|
|
49
49
|
<Text color={modeColor}>{node.mode} {modeIcon}</Text>
|
|
50
50
|
</Box>
|
|
51
51
|
<Box>
|
|
52
|
-
<
|
|
52
|
+
<Text color={statusColor}>
|
|
53
|
+
<StatusDot status={statusText} /> {statusText}
|
|
54
|
+
</Text>
|
|
53
55
|
</Box>
|
|
54
56
|
</Box>
|
|
55
57
|
|
|
@@ -9,7 +9,8 @@ const themes = [
|
|
|
9
9
|
{ id: 'amber', name: '3. Retro Amber', desc: 'old school CRT terminal feel' },
|
|
10
10
|
{ id: 'cyberpunk', name: '4. Cyberpunk', desc: 'neon pink and cyan highlights' },
|
|
11
11
|
{ id: 'hacker', name: '5. Hacker', desc: 'matrix style green on black' },
|
|
12
|
-
{ id: 'ocean', name: '6. Deep Ocean', desc: 'calming deep blues and seafoam' }
|
|
12
|
+
{ id: 'ocean', name: '6. Deep Ocean', desc: 'calming deep blues and seafoam' },
|
|
13
|
+
{ id: 'custom', name: '7. Custom JSON', desc: 'loads ~/.lcluster/custom.json' }
|
|
13
14
|
];
|
|
14
15
|
|
|
15
16
|
export default function ThemePicker({ onNext, updateConfig }) {
|
|
@@ -17,7 +17,7 @@ export default function Dashboard({ nodes, gatewayPort, uptimeStart, onAction })
|
|
|
17
17
|
<Box flexDirection="column" paddingX={2} paddingY={1} width="100%" height="100%" backgroundColor={theme.background}>
|
|
18
18
|
<Border borderColor={theme.border}>
|
|
19
19
|
<Box flexDirection="row" justifyContent="space-between" width="100%">
|
|
20
|
-
<Text color={theme.accent}> ⬡ lcluster v1.0.
|
|
20
|
+
<Text color={theme.accent}> ⬡ lcluster v1.0.2</Text>
|
|
21
21
|
<Box flexDirection="row" gap={2}>
|
|
22
22
|
<Text color={theme.online}>● {nodes.filter(n => n.status === 'online').length} online</Text>
|
|
23
23
|
<Text color={theme.degraded}>⚠ {nodes.filter(n => n.status === 'degraded' || n.status === 'reconnecting').length} warn</Text>
|
|
@@ -8,7 +8,7 @@ import { getTheme, updateTheme } from '../theme/index.js';
|
|
|
8
8
|
import Border from '../components/Border.jsx';
|
|
9
9
|
import TextInput from 'ink-text-input';
|
|
10
10
|
|
|
11
|
-
const themes = ['neon', 'minimal', 'amber', 'cyberpunk', 'hacker', 'ocean'];
|
|
11
|
+
const themes = ['neon', 'minimal', 'amber', 'cyberpunk', 'hacker', 'ocean', 'custom'];
|
|
12
12
|
|
|
13
13
|
export default function Settings({ onBack }) {
|
|
14
14
|
const configPath = path.join(os.homedir(), '.lcluster', 'config.yml');
|
|
@@ -32,7 +32,10 @@ export default function Settings({ onBack }) {
|
|
|
32
32
|
const [soundEnabled, setSoundEnabled] = useState(config.alerts?.sound?.enabled || false);
|
|
33
33
|
const [cpuWarn, setCpuWarn] = useState(String(config.alerts?.thresholds?.cpu_warn || 90));
|
|
34
34
|
|
|
35
|
-
const [
|
|
35
|
+
const [botEnabled, setBotEnabled] = useState(config.bot?.enabled || false);
|
|
36
|
+
const [botToken, setBotToken] = useState(config.bot?.token || '');
|
|
37
|
+
|
|
38
|
+
const [field, setField] = useState(0); // 0..9
|
|
36
39
|
|
|
37
40
|
const theme = getTheme();
|
|
38
41
|
|
|
@@ -41,7 +44,7 @@ export default function Settings({ onBack }) {
|
|
|
41
44
|
onBack();
|
|
42
45
|
}
|
|
43
46
|
if (key.upArrow) setField(Math.max(0, field - 1));
|
|
44
|
-
if (key.downArrow) setField(Math.min(
|
|
47
|
+
if (key.downArrow) setField(Math.min(9, field + 1));
|
|
45
48
|
|
|
46
49
|
if (field === 0) {
|
|
47
50
|
if (key.leftArrow) {
|
|
@@ -60,6 +63,7 @@ export default function Settings({ onBack }) {
|
|
|
60
63
|
if (field === 3) setDiscordEnabled(!discordEnabled);
|
|
61
64
|
else if (field === 5) setDesktopEnabled(!desktopEnabled);
|
|
62
65
|
else if (field === 6) setSoundEnabled(!soundEnabled);
|
|
66
|
+
else if (field === 8) setBotEnabled(!botEnabled);
|
|
63
67
|
else {
|
|
64
68
|
// Save config
|
|
65
69
|
const newConfig = {
|
|
@@ -74,6 +78,10 @@ export default function Settings({ onBack }) {
|
|
|
74
78
|
desktop: { enabled: desktopEnabled },
|
|
75
79
|
sound: { enabled: soundEnabled },
|
|
76
80
|
thresholds: { cpu_warn: parseInt(cpuWarn) || 90, idle_warn: false }
|
|
81
|
+
},
|
|
82
|
+
bot: {
|
|
83
|
+
enabled: botEnabled,
|
|
84
|
+
token: botToken
|
|
77
85
|
}
|
|
78
86
|
};
|
|
79
87
|
fs.writeFileSync(configPath, yaml.dump(newConfig), 'utf8');
|
|
@@ -135,11 +143,25 @@ export default function Settings({ onBack }) {
|
|
|
135
143
|
<TextInput value={cpuWarn} onChange={setCpuWarn} focus={field === 7} />
|
|
136
144
|
<Text color={theme.textDim}>%</Text>
|
|
137
145
|
</Box>
|
|
146
|
+
|
|
147
|
+
<Box marginTop={1}>
|
|
148
|
+
<Text color={theme.textDim}>CUSTOM BOT (v1.0.2)</Text>
|
|
149
|
+
</Box>
|
|
150
|
+
<Box>
|
|
151
|
+
<Text color={theme.textDim}>Active </Text>
|
|
152
|
+
{field === 8 ? <Text color={theme.accent}>▶ </Text> : <Text> </Text>}
|
|
153
|
+
<Text color={botEnabled ? theme.online : theme.textDim}>{botEnabled ? '● enabled' : '○ disabled'}</Text>
|
|
154
|
+
</Box>
|
|
155
|
+
<Box>
|
|
156
|
+
<Text color={theme.textDim}>Discord Token </Text>
|
|
157
|
+
{field === 9 ? <Text color={theme.accent}>▶ </Text> : <Text> </Text>}
|
|
158
|
+
<TextInput value={botToken} onChange={setBotToken} focus={field === 9} mask="•" />
|
|
159
|
+
</Box>
|
|
138
160
|
</Box>
|
|
139
161
|
<Box flexDirection="column" paddingX={1} marginTop={1}>
|
|
140
162
|
<Text color={theme.borderDim}>{'─'.repeat(45)}</Text>
|
|
141
163
|
<Box marginTop={1} flexDirection="column">
|
|
142
|
-
<Text color={theme.text} bold>lcluster v1.0.
|
|
164
|
+
<Text color={theme.text} bold>lcluster v1.0.2</Text>
|
|
143
165
|
<Text color={theme.textDim}>Built by <Text color={theme.text}>Ram Krishna</Text> & <Text color={theme.text}>Claude (Anthropic AI)</Text></Text>
|
|
144
166
|
<Text color={theme.textDim}>This project was designed and built with the help of AI.</Text>
|
|
145
167
|
</Box>
|
package/src/tui/theme/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import hacker from './hacker.js';
|
|
|
10
10
|
import ocean from './ocean.js';
|
|
11
11
|
|
|
12
12
|
let currentThemeConfig = 'neon';
|
|
13
|
+
let customThemeCache = null;
|
|
13
14
|
|
|
14
15
|
export function loadTheme() {
|
|
15
16
|
const configPath = path.join(os.homedir(), '.lcluster', 'config.yml');
|
|
@@ -28,6 +29,22 @@ export function updateTheme(name) {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function getTheme() {
|
|
32
|
+
if (currentThemeConfig === 'custom') {
|
|
33
|
+
if (!customThemeCache) {
|
|
34
|
+
const customPath = path.join(os.homedir(), '.lcluster', 'custom.json');
|
|
35
|
+
if (fs.existsSync(customPath)) {
|
|
36
|
+
try {
|
|
37
|
+
customThemeCache = JSON.parse(fs.readFileSync(customPath, 'utf8'));
|
|
38
|
+
} catch {
|
|
39
|
+
customThemeCache = neon; // Fallback bad json
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
customThemeCache = neon;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return customThemeCache;
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
switch (currentThemeConfig) {
|
|
32
49
|
case 'minimal': return minimal;
|
|
33
50
|
case 'amber': return amber;
|