lemonade-interactive-loader 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.
@@ -0,0 +1,359 @@
1
+ const inquirer = require('inquirer');
2
+ const { loadConfig } = require('../config');
3
+ const { getAllInstalledAssets, getLlamaServerPath, downloadAndExtractLlamaCpp, deleteInstalledAsset } = require('../services/asset-manager');
4
+ const { selectLlamaCppRelease, selectAsset, askLaunchServer } = require('./prompts');
5
+ const { runSetupWizard } = require('./setup-wizard');
6
+ const { launchLemonadeServer } = require('../services/server');
7
+ const { inferBackendType, formatBytes } = require('../utils/system');
8
+
9
+ /**
10
+ * Display and handle main menu selection
11
+ * @returns {Promise<string>} Selected command
12
+ */
13
+ async function showMainMenu() {
14
+ console.log('');
15
+ console.log('╔════════════════════════════════════════════════════════╗');
16
+ console.log('║ 🍋 Lemonade Interactive Launcher ║');
17
+ console.log('╚════════════════════════════════════════════════════════╝');
18
+ console.log('');
19
+
20
+ // Check if configuration exists
21
+ const config = loadConfig();
22
+ const hasConfig = Object.keys(config).length > 0;
23
+
24
+ // Show message if no configuration exists
25
+ if (!hasConfig) {
26
+ console.log('⚠️ No configuration found. Please run Setup first.\n');
27
+ }
28
+
29
+ // Build choices based on whether config exists
30
+ let choices;
31
+ if (hasConfig) {
32
+ choices = [
33
+ { name: '▶️ Start Server with Current Config', value: 'serve' },
34
+ { name: '✏️ Edit Configuration', value: 'edit' },
35
+ { name: '👁️ View Configuration', value: 'view' },
36
+ { name: '🔄 Reset Configuration', value: 'reset' },
37
+ new inquirer.Separator(' ──────────────────────────────────────'),
38
+ { name: '🚀 Setup - Configure Lemonade Server', value: 'setup' },
39
+ { name: '📦 Download Custom llama.cpp Builds', value: 'manage' }
40
+ ];
41
+ } else {
42
+ choices = [
43
+ { name: '🚀 Setup - Configure Lemonade Server', value: 'setup' },
44
+ { name: '📦 Download Custom llama.cpp Builds', value: 'manage' }
45
+ ];
46
+ }
47
+
48
+ const { command } = await inquirer.prompt([
49
+ {
50
+ type: 'list',
51
+ name: 'command',
52
+ message: 'What would you like to do?',
53
+ choices: choices
54
+ }
55
+ ]);
56
+
57
+ return command;
58
+ }
59
+
60
+ /**
61
+ * Display and handle manage submenu selection
62
+ * @returns {Promise<string>} Selected action
63
+ */
64
+ async function showManageMenu() {
65
+ const { manageAction } = await inquirer.prompt([
66
+ {
67
+ type: 'list',
68
+ name: 'manageAction',
69
+ message: 'What would you like to do?',
70
+ choices: [
71
+ { name: '👁️ View installed builds', value: 'view' },
72
+ { name: '🗑️ Delete installed build', value: 'delete' },
73
+ { name: '⬇️ Download new build', value: 'download' },
74
+ { name: '← Back to main menu', value: 'back' }
75
+ ]
76
+ }
77
+ ]);
78
+
79
+ return manageAction;
80
+ }
81
+
82
+ /**
83
+ * View all installed builds
84
+ */
85
+ async function viewInstalledBuilds() {
86
+ const installedAssets = getAllInstalledAssets();
87
+
88
+ if (installedAssets.length === 0) {
89
+ console.log('\nNo custom llama.cpp builds installed.');
90
+ return;
91
+ }
92
+
93
+ console.log('\n=== Installed Custom Builds ===\n');
94
+ installedAssets.forEach((asset, index) => {
95
+ const serverPath = getLlamaServerPath(asset.installPath);
96
+ console.log(`${index + 1}. ${asset.assetName}`);
97
+ console.log(` Path: ${asset.installPath}`);
98
+ console.log(` Backend: ${asset.backendType.toUpperCase()}`);
99
+ console.log(` Installed: ${new Date(asset.installTime).toLocaleString()}`);
100
+ console.log(` Server Binary: ${serverPath || 'Not found'}`);
101
+ console.log('');
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Delete installed builds interactively
107
+ */
108
+ async function deleteInstalledBuild() {
109
+ let continueDeleting = true;
110
+
111
+ while (continueDeleting) {
112
+ const installedAssets = getAllInstalledAssets();
113
+
114
+ if (installedAssets.length === 0) {
115
+ console.log('\nNo custom llama.cpp builds installed.');
116
+ break;
117
+ }
118
+
119
+ const choices = installedAssets.map((asset, index) => ({
120
+ name: `${asset.assetName} | Backend: ${asset.backendType.toUpperCase()} | ${new Date(asset.installTime).toLocaleDateString()}`,
121
+ value: index
122
+ }));
123
+
124
+ choices.unshift({
125
+ name: '← Cancel',
126
+ value: -1
127
+ });
128
+
129
+ const { deleteIndex } = await inquirer.prompt([
130
+ {
131
+ type: 'list',
132
+ name: 'deleteIndex',
133
+ message: 'Select a build to delete:',
134
+ choices: choices
135
+ }
136
+ ]);
137
+
138
+ if (deleteIndex < 0) {
139
+ break;
140
+ }
141
+
142
+ const assetToDelete = installedAssets[deleteIndex];
143
+ const { confirmDelete } = await inquirer.prompt([
144
+ {
145
+ type: 'confirm',
146
+ name: 'confirmDelete',
147
+ message: `Are you sure you want to delete "${assetToDelete.assetName}"? This cannot be undone.`,
148
+ default: false
149
+ }
150
+ ]);
151
+
152
+ if (confirmDelete) {
153
+ const success = deleteInstalledAsset(assetToDelete.installPath);
154
+ if (success) {
155
+ console.log(`✓ Deleted: ${assetToDelete.assetName}`);
156
+ }
157
+ }
158
+
159
+ const { deleteAnother } = await inquirer.prompt([
160
+ {
161
+ type: 'confirm',
162
+ name: 'deleteAnother',
163
+ message: 'Do you want to delete another build?',
164
+ default: false
165
+ }
166
+ ]);
167
+
168
+ if (!deleteAnother) {
169
+ continueDeleting = false;
170
+ }
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Download a new build interactively
176
+ */
177
+ async function downloadNewBuild() {
178
+ console.log('\nFetching recent llama.cpp builds...');
179
+ const release = await selectLlamaCppRelease();
180
+ const asset = await selectAsset(release);
181
+ const version = release.tag_name;
182
+ const installPath = await downloadAndExtractLlamaCpp(asset, version);
183
+
184
+ console.log(`\n✓ Build ready at: ${installPath}`);
185
+ console.log(` Backend Type: ${inferBackendType(asset.name).toUpperCase()}`);
186
+
187
+ const { downloadAnother } = await inquirer.prompt([
188
+ {
189
+ type: 'confirm',
190
+ name: 'downloadAnother',
191
+ message: 'Do you want to download another build?',
192
+ default: false
193
+ }
194
+ ]);
195
+
196
+ if (downloadAnother) {
197
+ await downloadNewBuild();
198
+ }
199
+ }
200
+
201
+ /**
202
+ * View current configuration
203
+ */
204
+ async function viewConfiguration() {
205
+ const config = loadConfig();
206
+
207
+ if (Object.keys(config).length === 0) {
208
+ console.log('No configuration found. Run "setup" to configure.');
209
+ return;
210
+ }
211
+
212
+ console.log('\n=== Current Configuration ===\n');
213
+ console.log(`Host: ${config.host}`);
214
+ console.log(`Port: ${config.port}`);
215
+ console.log(`Log Level: ${config.logLevel}`);
216
+ console.log(`Backend: ${config.backend}`);
217
+ console.log(`Model Directory: ${config.modelDir}`);
218
+ console.log(`Run Mode: ${config.runMode}`);
219
+ console.log(`llama.cpp Args: ${config.llamacppArgs || 'None'}`);
220
+
221
+ if (config.customLlamacppPath) {
222
+ console.log(`Custom llama.cpp Build: ${config.customLlamacppPath}`);
223
+ console.log(` Backend Type: ${config.customBackendType?.toUpperCase() || 'Unknown'}`);
224
+ console.log(` Server Binary: ${config.customServerPath || 'Not found'}`);
225
+ } else {
226
+ console.log(`Custom llama.cpp Build: Using bundled build`);
227
+ }
228
+
229
+ const installedAssets = getAllInstalledAssets();
230
+ if (installedAssets.length > 0) {
231
+ console.log('\n=== All Installed Custom Builds ===\n');
232
+ installedAssets.forEach((asset, index) => {
233
+ const serverPath = getLlamaServerPath(asset.installPath);
234
+ console.log(`${index + 1}. ${asset.assetName}`);
235
+ console.log(` Path: ${asset.installPath}`);
236
+ console.log(` Backend: ${asset.backendType.toUpperCase()}`);
237
+ console.log(` Installed: ${new Date(asset.installTime).toLocaleString()}`);
238
+ console.log(` Server Binary: ${serverPath || 'Not found'}`);
239
+ console.log('');
240
+ });
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Reset configuration
246
+ */
247
+ async function resetConfiguration() {
248
+ const { confirmReset } = await inquirer.prompt([
249
+ {
250
+ type: 'confirm',
251
+ name: 'confirmReset',
252
+ message: 'Are you sure you want to reset all configuration? This cannot be undone.',
253
+ default: false
254
+ }
255
+ ]);
256
+
257
+ if (confirmReset) {
258
+ const { resetConfig } = require('../config');
259
+ resetConfig();
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Handle main menu command
265
+ * @param {string} command - Selected command
266
+ */
267
+ async function handleCommand(command) {
268
+ switch (command) {
269
+ case 'setup':
270
+ await runSetupWizard(false);
271
+ if (await askLaunchServer()) {
272
+ const config = loadConfig();
273
+ if (Object.keys(config).length > 0) {
274
+ await launchLemonadeServer(config);
275
+ }
276
+ }
277
+ break;
278
+
279
+ case 'edit':
280
+ await runSetupWizard(true);
281
+ if (await askLaunchServer()) {
282
+ const config = loadConfig();
283
+ if (Object.keys(config).length > 0) {
284
+ await launchLemonadeServer(config);
285
+ }
286
+ }
287
+ break;
288
+
289
+ case 'view':
290
+ await viewConfiguration();
291
+ break;
292
+
293
+ case 'reset':
294
+ await resetConfiguration();
295
+ break;
296
+
297
+
298
+
299
+ case 'manage':
300
+ let manageAction;
301
+ do {
302
+ manageAction = await showManageMenu();
303
+
304
+ switch (manageAction) {
305
+ case 'view':
306
+ await viewInstalledBuilds();
307
+ break;
308
+ case 'delete':
309
+ await deleteInstalledBuild();
310
+ break;
311
+ case 'download':
312
+ await downloadNewBuild();
313
+ break;
314
+ }
315
+ } while (manageAction !== 'back');
316
+ break;
317
+
318
+ case 'serve':
319
+ const config = loadConfig();
320
+ if (Object.keys(config).length === 0) {
321
+ console.log('No configuration found. Please run "setup" first.');
322
+ return;
323
+ }
324
+ await launchLemonadeServer(config);
325
+ break;
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Run the main CLI loop
331
+ */
332
+ async function runCLI() {
333
+ let continueRunning = true;
334
+
335
+ while (continueRunning) {
336
+ const command = await showMainMenu();
337
+ await handleCommand(command);
338
+
339
+ const { continueRunning: shouldContinue } = await inquirer.prompt([
340
+ {
341
+ type: 'confirm',
342
+ name: 'continueRunning',
343
+ message: 'Would you like to return to the main menu?',
344
+ default: true
345
+ }
346
+ ]);
347
+
348
+ continueRunning = shouldContinue;
349
+ }
350
+
351
+ console.log('\n👋 Goodbye!\n');
352
+ }
353
+
354
+ module.exports = {
355
+ showMainMenu,
356
+ showManageMenu,
357
+ handleCommand,
358
+ runCLI
359
+ };
@@ -0,0 +1,247 @@
1
+ const inquirer = require('inquirer');
2
+ const { DEFAULTS, BACKEND_TYPES, LOG_LEVELS, RUN_MODES, HOSTS } = require('../config/constants');
3
+ const { loadConfig } = require('../config');
4
+ const { detectSystem, formatBytes, filterServerAssets, inferBackendType } = require('../utils/system');
5
+ const { fetchAllReleases } = require('../services/github');
6
+ const { getAllInstalledAssets, getLlamaServerPath, downloadAndExtractLlamaCpp, selectInstalledAsset, isAssetInstalled } = require('../services/asset-manager');
7
+
8
+ /**
9
+ * Select a llama.cpp release from list
10
+ * @returns {Promise<Object>} Selected release
11
+ */
12
+ async function selectLlamaCppRelease() {
13
+ console.log('\nFetching available releases...');
14
+
15
+ let releases;
16
+ try {
17
+ releases = await fetchAllReleases(20);
18
+ console.log(`Found ${releases.length} releases.\n`);
19
+ } catch (error) {
20
+ console.error(`Error fetching releases: ${error.message}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ const releaseChoices = releases.map(release => {
25
+ const serverAssets = filterServerAssets(release.assets);
26
+ const installedCount = serverAssets.filter(asset =>
27
+ isAssetInstalled(release.tag_name, asset.name)
28
+ ).length;
29
+
30
+ const totalAssets = serverAssets.length;
31
+ let status = '';
32
+ if (installedCount === totalAssets && totalAssets > 0) {
33
+ status = ' ✓ All assets installed';
34
+ } else if (installedCount > 0) {
35
+ status = ` (${installedCount}/${totalAssets} installed)`;
36
+ }
37
+
38
+ return {
39
+ name: `${release.tag_name} - ${new Date(release.published_at).toLocaleDateString()}${status}`,
40
+ value: release
41
+ };
42
+ });
43
+
44
+ const { selectedRelease } = await inquirer.prompt([
45
+ {
46
+ type: 'list',
47
+ name: 'selectedRelease',
48
+ message: 'Select a release:',
49
+ choices: releaseChoices
50
+ }
51
+ ]);
52
+
53
+ return selectedRelease;
54
+ }
55
+
56
+ /**
57
+ * Select an asset from a release
58
+ * @param {Object} release - Release object
59
+ * @returns {Promise<Object>} Selected asset
60
+ */
61
+ async function selectAsset(release) {
62
+ const systemInfo = detectSystem();
63
+ const serverAssets = filterServerAssets(release.assets);
64
+
65
+ const byPlatform = {};
66
+ serverAssets.forEach(asset => {
67
+ let platform = 'Other';
68
+ const name = asset.name.toLowerCase();
69
+
70
+ if (name.includes('win') || name.includes('windows')) platform = 'Windows';
71
+ else if (name.includes('ubuntu') || name.includes('linux')) platform = 'Linux';
72
+ else if (name.includes('macos') || name.includes('mac')) platform = 'macOS';
73
+ else if (name.includes('rocm')) platform = 'ROCm (Linux)';
74
+ else if (name.includes('cuda')) platform = 'CUDA (Linux)';
75
+
76
+ if (!byPlatform[platform]) {
77
+ byPlatform[platform] = [];
78
+ }
79
+ byPlatform[platform].push(asset);
80
+ });
81
+
82
+ const assetChoices = [];
83
+ for (const [platform, assets] of Object.entries(byPlatform)) {
84
+ assetChoices.push({
85
+ name: `── ${platform} ──`,
86
+ disabled: true
87
+ });
88
+
89
+ assets.forEach(asset => {
90
+ const name = asset.name.toLowerCase();
91
+ let isCurrentPlatform = false;
92
+
93
+ if (systemInfo.osType === 'windows' && platform === 'Windows') {
94
+ if (systemInfo.arch === 'x64' && name.includes('x64')) isCurrentPlatform = true;
95
+ else if (systemInfo.arch === 'arm64' && name.includes('arm64')) isCurrentPlatform = true;
96
+ else if (!name.includes('x64') && !name.includes('arm64')) isCurrentPlatform = true;
97
+ } else if (systemInfo.osType === 'linux' && platform === 'Linux') {
98
+ if (systemInfo.arch === 'x64' && name.includes('x64')) isCurrentPlatform = true;
99
+ else if (systemInfo.arch === 'arm64' && name.includes('aarch64')) isCurrentPlatform = true;
100
+ else if (!name.includes('x64') && !name.includes('aarch64')) isCurrentPlatform = true;
101
+ } else if (systemInfo.osType === 'macos' && platform === 'macOS') {
102
+ if (systemInfo.arch === 'arm64' && (name.includes('arm64') || name.includes('aarch64'))) isCurrentPlatform = true;
103
+ else if (systemInfo.arch === 'x64' && name.includes('x64')) isCurrentPlatform = true;
104
+ else if (!name.includes('arm64') && !name.includes('x64')) isCurrentPlatform = true;
105
+ } else if (systemInfo.osType === 'linux' && platform.includes('ROCm')) {
106
+ if (systemInfo.arch === 'x64') isCurrentPlatform = true;
107
+ } else if (systemInfo.osType === 'linux' && platform.includes('CUDA')) {
108
+ if (systemInfo.arch === 'x64') isCurrentPlatform = true;
109
+ }
110
+
111
+ const version = release.tag_name;
112
+ const isInstalled = isAssetInstalled(version, asset.name);
113
+
114
+ let marker = '';
115
+ if (isInstalled) {
116
+ marker = ' ✓ Already installed';
117
+ } else if (isCurrentPlatform) {
118
+ marker = ' ← Best match';
119
+ }
120
+
121
+ assetChoices.push({
122
+ name: `${asset.name} (${formatBytes(asset.size)})${marker}`,
123
+ value: asset,
124
+ disabled: false
125
+ });
126
+ });
127
+ }
128
+
129
+ const { selectedAsset } = await inquirer.prompt([
130
+ {
131
+ type: 'list',
132
+ name: 'selectedAsset',
133
+ message: 'Select the asset to download:',
134
+ choices: assetChoices
135
+ }
136
+ ]);
137
+
138
+ return selectedAsset;
139
+ }
140
+
141
+ /**
142
+ * Select from installed assets
143
+ * @returns {Promise<Object|null>} Selected asset or null
144
+ */
145
+ async function selectInstalledAssetPrompt() {
146
+ const installedAssets = getAllInstalledAssets();
147
+
148
+ if (installedAssets.length === 0) {
149
+ console.log('\nNo custom llama.cpp builds installed.');
150
+ return null;
151
+ }
152
+
153
+ const choices = installedAssets.map((asset, index) => {
154
+ const installDate = new Date(asset.installTime).toLocaleString();
155
+ const backendType = asset.backendType.toUpperCase();
156
+ const serverPath = getLlamaServerPath(asset.installPath);
157
+ const hasServer = serverPath ? '✓' : '✗';
158
+
159
+ return {
160
+ name: `[${index + 1}] ${asset.assetName} | Backend: ${backendType} | Installed: ${installDate} | Server: ${hasServer}`,
161
+ value: asset
162
+ };
163
+ });
164
+
165
+ choices.unshift({
166
+ name: '── Skip - Use bundled build ──',
167
+ value: null,
168
+ disabled: false
169
+ });
170
+
171
+ const { selectedInstalled } = await inquirer.prompt([
172
+ {
173
+ type: 'list',
174
+ name: 'selectedInstalled',
175
+ message: 'Select an installed custom build (or skip):',
176
+ choices: choices
177
+ }
178
+ ]);
179
+
180
+ if (selectedInstalled) {
181
+ const serverPath = getLlamaServerPath(selectedInstalled.installPath);
182
+ if (!serverPath) {
183
+ console.log('\n⚠️ Warning: llama-server binary not found in this installation.');
184
+ console.log(' If you selected "auto" backend, it may not use this binary.');
185
+ } else {
186
+ console.log(`\n✓ Selected: ${selectedInstalled.assetName}`);
187
+ console.log(` Backend Type: ${selectedInstalled.backendType.toUpperCase()}`);
188
+ console.log(` Server Binary: ${serverPath}`);
189
+
190
+ return {
191
+ ...selectedInstalled,
192
+ serverPath
193
+ };
194
+ }
195
+ }
196
+
197
+ return null;
198
+ }
199
+
200
+ /**
201
+ * Ask if user wants to launch the server
202
+ * @returns {Promise<boolean>}
203
+ */
204
+ async function askLaunchServer() {
205
+ const { launchNow } = await inquirer.prompt([
206
+ {
207
+ type: 'confirm',
208
+ name: 'launchNow',
209
+ message: 'Would you like to launch the server now?',
210
+ default: true
211
+ }
212
+ ]);
213
+
214
+ return launchNow;
215
+ }
216
+
217
+ /**
218
+ * Display configuration summary
219
+ * @param {Object} config - Configuration object
220
+ */
221
+ function displayConfigSummary(config) {
222
+ console.log('\n=== Configuration Summary ===\n');
223
+ console.log(`Host: ${config.host}`);
224
+ console.log(`Port: ${config.port}`);
225
+ console.log(`Log Level: ${config.logLevel}`);
226
+ console.log(`Backend: ${config.backend}`);
227
+ console.log(`Model Directory: ${config.modelDir}`);
228
+ console.log(`Run Mode: ${config.runMode}`);
229
+ console.log(`llama.cpp Args: ${config.llamacppArgs || 'None'}`);
230
+
231
+ if (config.customLlamacppPath) {
232
+ console.log(`Custom llama.cpp Build: ${config.customLlamacppPath}`);
233
+ console.log(` Backend Type: ${config.customBackendType?.toUpperCase() || 'Unknown'}`);
234
+ console.log(` Server Binary: ${config.customServerPath || 'Not found'}`);
235
+ } else {
236
+ console.log(`Custom llama.cpp Build: Using bundled build`);
237
+ }
238
+ console.log('');
239
+ }
240
+
241
+ module.exports = {
242
+ selectLlamaCppRelease,
243
+ selectAsset,
244
+ selectInstalledAssetPrompt,
245
+ askLaunchServer,
246
+ displayConfigSummary
247
+ };