lcluster 1.0.0

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.
Files changed (58) hide show
  1. package/README.md +173 -0
  2. package/built-in-templates/default.yml +14 -0
  3. package/built-in-templates/high-memory.yml +14 -0
  4. package/built-in-templates/minimal.yml +14 -0
  5. package/generate_logos.js +39 -0
  6. package/package.json +52 -0
  7. package/src/alerts/desktop.js +15 -0
  8. package/src/alerts/discord.js +53 -0
  9. package/src/alerts/index.js +50 -0
  10. package/src/alerts/sound.js +5 -0
  11. package/src/cli.js +25 -0
  12. package/src/core/config.js +17 -0
  13. package/src/core/events.js +5 -0
  14. package/src/core/healthcheck.js +126 -0
  15. package/src/core/loadbalancer.js +26 -0
  16. package/src/core/logBuffer.js +35 -0
  17. package/src/core/registry.js +59 -0
  18. package/src/gateway/guildMap.js +6 -0
  19. package/src/gateway/proxy.js +30 -0
  20. package/src/gateway/router.js +6 -0
  21. package/src/gateway/server.js +35 -0
  22. package/src/gateway/sessionMap.js +6 -0
  23. package/src/gateway/v4/rest.js +79 -0
  24. package/src/gateway/v4/websocket.js +95 -0
  25. package/src/main.js +133 -0
  26. package/src/spawner/docker.js +55 -0
  27. package/src/spawner/process.js +43 -0
  28. package/src/system/detect.js +74 -0
  29. package/src/system/systemd.js +33 -0
  30. package/src/templates/manager.js +66 -0
  31. package/src/templates/validator.js +36 -0
  32. package/src/tui/components/Border.jsx +27 -0
  33. package/src/tui/components/KeyHints.jsx +20 -0
  34. package/src/tui/components/MiniBar.jsx +22 -0
  35. package/src/tui/components/NodeCard.jsx +65 -0
  36. package/src/tui/components/NodeList.jsx +60 -0
  37. package/src/tui/components/StatPanel.jsx +64 -0
  38. package/src/tui/components/StatusDot.jsx +22 -0
  39. package/src/tui/index.jsx +69 -0
  40. package/src/tui/init/DiscordAlerts.jsx +122 -0
  41. package/src/tui/init/Done.jsx +77 -0
  42. package/src/tui/init/GatewaySetup.jsx +59 -0
  43. package/src/tui/init/NodeSetup.jsx +158 -0
  44. package/src/tui/init/ThemePicker.jsx +51 -0
  45. package/src/tui/init/Welcome.jsx +57 -0
  46. package/src/tui/init/index.jsx +78 -0
  47. package/src/tui/screens/Dashboard.jsx +51 -0
  48. package/src/tui/screens/Logs.jsx +59 -0
  49. package/src/tui/screens/NodeDetail.jsx +57 -0
  50. package/src/tui/screens/Settings.jsx +154 -0
  51. package/src/tui/screens/Templates.jsx +42 -0
  52. package/src/tui/theme/amber.js +16 -0
  53. package/src/tui/theme/cyberpunk.js +15 -0
  54. package/src/tui/theme/hacker.js +15 -0
  55. package/src/tui/theme/index.js +42 -0
  56. package/src/tui/theme/minimal.js +16 -0
  57. package/src/tui/theme/neon.js +16 -0
  58. package/src/tui/theme/ocean.js +15 -0
@@ -0,0 +1,154 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
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
+ import { getTheme, updateTheme } from '../theme/index.js';
8
+ import Border from '../components/Border.jsx';
9
+ import TextInput from 'ink-text-input';
10
+
11
+ const themes = ['neon', 'minimal', 'amber', 'cyberpunk', 'hacker', 'ocean'];
12
+
13
+ export default function Settings({ onBack }) {
14
+ const configPath = path.join(os.homedir(), '.lcluster', 'config.yml');
15
+ const [config, setConfig] = useState(() => {
16
+ if (fs.existsSync(configPath)) {
17
+ try { return yaml.load(fs.readFileSync(configPath, 'utf8')) || {}; } catch { }
18
+ }
19
+ return {};
20
+ });
21
+
22
+ const [themeIdx, setThemeIdx] = useState(
23
+ themes.indexOf(config.theme || 'neon') !== -1 ? themes.indexOf(config.theme || 'neon') : 0
24
+ );
25
+
26
+ const [portStr, setPortStr] = useState(String(config.gateway?.port || 2333));
27
+ const [passStr, setPassStr] = useState(config.gateway?.password || 'youshallnotpass');
28
+
29
+ const [discordUrl, setDiscordUrl] = useState(config.alerts?.discord?.webhook || '');
30
+ const [discordEnabled, setDiscordEnabled] = useState(config.alerts?.discord?.enabled || false);
31
+ const [desktopEnabled, setDesktopEnabled] = useState(config.alerts?.desktop?.enabled || false);
32
+ const [soundEnabled, setSoundEnabled] = useState(config.alerts?.sound?.enabled || false);
33
+ const [cpuWarn, setCpuWarn] = useState(String(config.alerts?.thresholds?.cpu_warn || 90));
34
+
35
+ const [field, setField] = useState(0); // 0..7
36
+
37
+ const theme = getTheme();
38
+
39
+ useInput((input, key) => {
40
+ if (key.escape || (input === 'q' && field === 0)) {
41
+ onBack();
42
+ }
43
+ if (key.upArrow) setField(Math.max(0, field - 1));
44
+ if (key.downArrow) setField(Math.min(7, field + 1));
45
+
46
+ if (field === 0) {
47
+ if (key.leftArrow) {
48
+ const newIdx = Math.max(0, themeIdx - 1);
49
+ setThemeIdx(newIdx);
50
+ updateTheme(themes[newIdx]);
51
+ }
52
+ if (key.rightArrow) {
53
+ const newIdx = Math.min(themes.length - 1, themeIdx + 1);
54
+ setThemeIdx(newIdx);
55
+ updateTheme(themes[newIdx]);
56
+ }
57
+ }
58
+
59
+ if (key.return) {
60
+ if (field === 3) setDiscordEnabled(!discordEnabled);
61
+ else if (field === 5) setDesktopEnabled(!desktopEnabled);
62
+ else if (field === 6) setSoundEnabled(!soundEnabled);
63
+ else {
64
+ // Save config
65
+ const newConfig = {
66
+ ...config,
67
+ theme: themes[themeIdx],
68
+ gateway: {
69
+ port: parseInt(portStr),
70
+ password: passStr
71
+ },
72
+ alerts: {
73
+ discord: { enabled: discordEnabled, webhook: discordUrl },
74
+ desktop: { enabled: desktopEnabled },
75
+ sound: { enabled: soundEnabled },
76
+ thresholds: { cpu_warn: parseInt(cpuWarn) || 90, idle_warn: false }
77
+ }
78
+ };
79
+ fs.writeFileSync(configPath, yaml.dump(newConfig), 'utf8');
80
+ onBack();
81
+ }
82
+ }
83
+ });
84
+
85
+ return (
86
+ <Box flexDirection="column" width="100%" height="100%" backgroundColor={theme.background}>
87
+ <Border title="settings" borderColor={theme.border} flexGrow={1}>
88
+ <Box flexDirection="column" paddingX={1} marginY={1}>
89
+ <Text color={theme.textDim}>APPEARANCE</Text>
90
+ </Box>
91
+ <Box marginBottom={1}>
92
+ <Text color={theme.textDim}>Theme </Text>
93
+ <Text color={field === 0 ? theme.text : theme.textDim}>
94
+ {field === 0 && <Text color={theme.accent}>▶ </Text>}
95
+ {'< '} {themes[themeIdx].padEnd(8)} {' >'}
96
+ </Text>
97
+ </Box>
98
+ <Box>
99
+ <Text color={theme.textDim}>GATEWAY</Text>
100
+ </Box>
101
+ <Box>
102
+ <Text color={theme.textDim}>Port </Text>
103
+ {field === 1 ? <Text color={theme.accent}>▶ </Text> : <Text> </Text>}
104
+ <TextInput value={portStr} onChange={setPortStr} focus={field === 1} />
105
+ </Box>
106
+ <Box marginBottom={1}>
107
+ <Text color={theme.textDim}>Password </Text>
108
+ {field === 2 ? <Text color={theme.accent}>▶ </Text> : <Text> </Text>}
109
+ <TextInput value={passStr} onChange={setPassStr} focus={field === 2} mask="•" />
110
+ </Box>
111
+
112
+ <Box>
113
+ <Text color={theme.textDim}>ALERTS</Text>
114
+ </Box>
115
+ <Box>
116
+ <Text color={theme.textDim}>Discord </Text>
117
+ {field === 3 ? <Text color={theme.accent}>▶ </Text> : <Text> </Text>}
118
+ <Text color={discordEnabled ? theme.online : theme.textDim}>{discordEnabled ? '● enabled' : '○ disabled'}</Text>
119
+ <Text color={theme.textDim}> webhook → </Text>
120
+ {field === 4 ? <Text color={theme.accent}>▶ </Text> : <Text> </Text>}
121
+ <TextInput value={discordUrl} onChange={setDiscordUrl} focus={field === 4} />
122
+ </Box>
123
+ <Box>
124
+ <Text color={theme.textDim}>Desktop </Text>
125
+ {field === 5 ? <Text color={theme.accent}>▶ </Text> : <Text> </Text>}
126
+ <Text color={desktopEnabled ? theme.online : theme.textDim}>{desktopEnabled ? '● enabled' : '○ disabled'}</Text>
127
+ </Box>
128
+ <Box>
129
+ <Text color={theme.textDim}>Sound </Text>
130
+ {field === 6 ? <Text color={theme.accent}>▶ </Text> : <Text> </Text>}
131
+ <Text color={soundEnabled ? theme.online : theme.textDim}>{soundEnabled ? '● enabled' : '○ disabled'}</Text>
132
+ </Box>
133
+ <Box>
134
+ <Text color={theme.textDim}>CPU threshold </Text>
135
+ {field === 7 ? <Text color={theme.accent}>▶ </Text> : <Text> </Text>}
136
+ <TextInput value={cpuWarn} onChange={setCpuWarn} focus={field === 7} />
137
+ <Text color={theme.textDim}>%</Text>
138
+ </Box>
139
+ </Box>
140
+ <Box flexDirection="column" paddingX={1} marginTop={1}>
141
+ <Text color={theme.borderDim}>{'─'.repeat(45)}</Text>
142
+ <Box marginTop={1} flexDirection="column">
143
+ <Text color={theme.text} bold>lcluster v1.0.0</Text>
144
+ <Text color={theme.textDim}>Built by <Text color={theme.text}>Ram Krishna</Text> & <Text color={theme.text}>Claude (Anthropic AI)</Text></Text>
145
+ <Text color={theme.textDim}>This project was designed and built with the help of AI.</Text>
146
+ </Box>
147
+ </Box>
148
+ </Border >
149
+ <Box marginTop={1}>
150
+ <Text color={theme.textDim}>[↑↓] fields [←→] change theme [enter] save [q/esc] cancel</Text>
151
+ </Box>
152
+ </Box >
153
+ );
154
+ }
@@ -0,0 +1,42 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { getTheme } from '../theme/index.js';
4
+ import Border from '../components/Border.jsx';
5
+ import { listTemplates } from '../../templates/manager.js';
6
+
7
+ export default function Templates({ onBack }) {
8
+ const theme = getTheme();
9
+ const [templates] = useState(() => listTemplates());
10
+ const [selectedIndex, setSelectedIndex] = useState(0);
11
+
12
+ useInput((input, key) => {
13
+ if (key.upArrow) setSelectedIndex(Math.max(0, selectedIndex - 1));
14
+ if (key.downArrow) setSelectedIndex(Math.min(templates.length, selectedIndex + 1));
15
+ if (input === 'q' || key.escape) onBack();
16
+ // In full impl [enter] would open editor or delete
17
+ });
18
+
19
+ return (
20
+ <Box flexDirection="column" width="100%" height="100%" backgroundColor={theme.background}>
21
+ <Border title="templates" borderColor={theme.border} flexGrow={1}>
22
+ <Box flexDirection="column" paddingX={1} flexGrow={1}>
23
+ {templates.map((t, i) => (
24
+ <Text key={t.name} color={selectedIndex === i ? theme.text : theme.textDim} bold={selectedIndex === i}>
25
+ {selectedIndex === i ? <Text color={theme.accent}>▶ </Text> : ' '}
26
+ {t.name.padEnd(20)} {t.builtin && <Text color={theme.textDim}>(built-in)</Text>}
27
+ </Text>
28
+ ))}
29
+ <Box marginTop={1}>
30
+ <Text color={selectedIndex === templates.length ? theme.text : theme.textDim} bold={selectedIndex === templates.length}>
31
+ {selectedIndex === templates.length ? <Text color={theme.accent}>▶ </Text> : ' '}
32
+ + Add new template
33
+ </Text>
34
+ </Box>
35
+ </Box>
36
+ </Border>
37
+ <Box marginTop={1}>
38
+ <Text color={theme.textDim}>[↑↓] navigate [enter] select / edit [d] delete (user only) [q] back</Text>
39
+ </Box>
40
+ </Box>
41
+ );
42
+ }
@@ -0,0 +1,16 @@
1
+ export default {
2
+ border: '#ffb347',
3
+ borderDim: '#ffb34733',
4
+ text: '#ffb347',
5
+ textDim: '#ff8c0066',
6
+ background: '#0d0800',
7
+ selected: '#ffb347',
8
+ selectedBorder: '#ff8c00',
9
+ online: '#ffb347',
10
+ degraded: '#ff8c00',
11
+ offline: '#8b4513',
12
+ docker: '#ffd700',
13
+ process: '#ffb347',
14
+ accent: '#ff8c00',
15
+ keyHint: '#ff8c0044',
16
+ };
@@ -0,0 +1,15 @@
1
+ const cyberpunk = {
2
+ background: '#0B0B1A',
3
+ text: '#E0E0E0',
4
+ textDim: '#6b7280',
5
+ border: '#FF0055',
6
+ borderDim: '#4A0033',
7
+ accent: '#00FFCC',
8
+ error: '#FF0055',
9
+ warning: '#FFEA00',
10
+ online: '#00FFCC',
11
+ degraded: '#FFEA00',
12
+ offline: '#FF0055'
13
+ };
14
+
15
+ export default cyberpunk;
@@ -0,0 +1,15 @@
1
+ const hacker = {
2
+ background: '#000000',
3
+ text: '#00FF00',
4
+ textDim: '#005500',
5
+ border: '#00FF00',
6
+ borderDim: '#003300',
7
+ accent: '#FFFFFF',
8
+ error: '#FF0000',
9
+ warning: '#FFFF00',
10
+ online: '#00FF00',
11
+ degraded: '#FFFF00',
12
+ offline: '#FF0000'
13
+ };
14
+
15
+ export default hacker;
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import yaml from 'js-yaml';
5
+ import neon from './neon.js';
6
+ import minimal from './minimal.js';
7
+ import amber from './amber.js';
8
+ import cyberpunk from './cyberpunk.js';
9
+ import hacker from './hacker.js';
10
+ import ocean from './ocean.js';
11
+
12
+ let currentThemeConfig = 'neon';
13
+
14
+ export function loadTheme() {
15
+ const configPath = path.join(os.homedir(), '.lcluster', 'config.yml');
16
+ if (fs.existsSync(configPath)) {
17
+ try {
18
+ const doc = yaml.load(fs.readFileSync(configPath, 'utf8'));
19
+ if (doc && doc.theme) {
20
+ currentThemeConfig = doc.theme;
21
+ }
22
+ } catch { }
23
+ }
24
+ }
25
+
26
+ export function updateTheme(name) {
27
+ currentThemeConfig = name;
28
+ }
29
+
30
+ export function getTheme() {
31
+ switch (currentThemeConfig) {
32
+ case 'minimal': return minimal;
33
+ case 'amber': return amber;
34
+ case 'cyberpunk': return cyberpunk;
35
+ case 'hacker': return hacker;
36
+ case 'ocean': return ocean;
37
+ case 'neon':
38
+ default: return neon;
39
+ }
40
+ }
41
+
42
+ export { currentThemeConfig };
@@ -0,0 +1,16 @@
1
+ export default {
2
+ border: '#45475a',
3
+ borderDim: '#313244',
4
+ text: '#cdd6f4',
5
+ textDim: '#6c7086',
6
+ background: '#1e1e2e',
7
+ selected: '#313244',
8
+ selectedBorder: '#45475a',
9
+ online: '#a6e3a1',
10
+ degraded: '#f9e2af',
11
+ offline: '#f38ba8',
12
+ docker: '#89b4fa',
13
+ process: '#a6e3a1',
14
+ accent: '#cba6f7',
15
+ keyHint: '#45475a',
16
+ };
@@ -0,0 +1,16 @@
1
+ export default {
2
+ border: '#00ff9f',
3
+ borderDim: '#00ff9f33',
4
+ text: '#00ff9f',
5
+ textDim: '#00ff9f66',
6
+ background: '#080b14',
7
+ selected: '#00ff9f11',
8
+ selectedBorder: '#00ff9f55',
9
+ online: '#00ff9f',
10
+ degraded: '#ffcc00',
11
+ offline: '#ff3366',
12
+ docker: '#4facfe',
13
+ process: '#a8edea',
14
+ accent: '#00ff9f',
15
+ keyHint: '#00ff9f33',
16
+ };
@@ -0,0 +1,15 @@
1
+ const ocean = {
2
+ background: '#0B132B',
3
+ text: '#E0FBFC',
4
+ textDim: '#5C6B73',
5
+ border: '#3A506B',
6
+ borderDim: '#1C2541',
7
+ accent: '#5BC0BE',
8
+ error: '#FF6B6B',
9
+ warning: '#F4D35E',
10
+ online: '#5BC0BE',
11
+ degraded: '#F4D35E',
12
+ offline: '#FF6B6B'
13
+ };
14
+
15
+ export default ocean;