lemonade-interactive-loader 1.0.1 → 1.0.3

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 CHANGED
@@ -73,7 +73,7 @@ The main menu adapts based on whether you have a configuration saved:
73
73
  ```
74
74
 
75
75
  | Command | Description |
76
- |---------|-------------||
76
+ |---------|-------------|
77
77
  | **▶️ Start Server** | Launch Lemonade Server with saved config (when config exists) |
78
78
  | **✏️ Edit Configuration** | Update your saved settings interactively |
79
79
  | **👁️ View Configuration** | See your current configuration and installed builds |
@@ -169,7 +169,7 @@ Point Lemonade Server to your existing model directory (like LM Studio's):
169
169
 
170
170
  Configuration is automatically saved and loaded:
171
171
 
172
- - **Location**: `~/.lemonade-launcher/config.json` (Linux/macOS) or `%USERPROFILE%\.lemonade-launcher\config.json` (Windows)
172
+ - **Location**: `~/.lemonade-interactive-launcher/config.json` (Linux/macOS) or `%USERPROFILE%\.lemonade-interactive-launcher\config.json` (Windows)
173
173
  - **Format**: JSON
174
174
  - **Auto-saved**: After every setup or edit
175
175
 
@@ -250,7 +250,7 @@ Run Command Prompt or PowerShell as Administrator.
250
250
 
251
251
  #### Build download fails
252
252
  - Check your internet connection
253
- - Ensure you have write permissions to `~/.lemonade-launcher/`
253
+ - Ensure you have write permissions to `~/.lemonade-interactive-launcher/`
254
254
  - Try downloading the asset manually from GitHub
255
255
 
256
256
  #### Server won't start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemonade-interactive-loader",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Interactive CLI tool to launch Lemonade Server with custom arguments and download llama.cpp releases - Cross-platform (Windows/Linux)",
5
5
  "main": "index.js",
6
6
  "bin": {
package/src/cli/menu.js CHANGED
@@ -3,7 +3,7 @@ const { loadConfig } = require('../config');
3
3
  const { getAllInstalledAssets, getLlamaServerPath, downloadAndExtractLlamaCpp, deleteInstalledAsset } = require('../services/asset-manager');
4
4
  const { selectLlamaCppRelease, selectAsset, askLaunchServer } = require('./prompts');
5
5
  const { runSetupWizard } = require('./setup-wizard');
6
- const { launchLemonadeServer } = require('../services/server');
6
+ const { launchLemonadeServer, setupShutdownHandlers } = require('../services/server');
7
7
  const { inferBackendType, formatBytes } = require('../utils/system');
8
8
 
9
9
  /**
@@ -263,15 +263,20 @@ async function resetConfiguration() {
263
263
  /**
264
264
  * Handle main menu command
265
265
  * @param {string} command - Selected command
266
+ * @returns {Promise<boolean>} True if should exit the app
266
267
  */
267
268
  async function handleCommand(command) {
269
+ let shouldExit = false;
270
+
268
271
  switch (command) {
269
272
  case 'setup':
270
273
  await runSetupWizard(false);
271
274
  if (await askLaunchServer()) {
272
275
  const config = loadConfig();
273
276
  if (Object.keys(config).length > 0) {
277
+ setupShutdownHandlers();
274
278
  await launchLemonadeServer(config);
279
+ shouldExit = true;
275
280
  }
276
281
  }
277
282
  break;
@@ -281,7 +286,9 @@ async function handleCommand(command) {
281
286
  if (await askLaunchServer()) {
282
287
  const config = loadConfig();
283
288
  if (Object.keys(config).length > 0) {
289
+ setupShutdownHandlers();
284
290
  await launchLemonadeServer(config);
291
+ shouldExit = true;
285
292
  }
286
293
  }
287
294
  break;
@@ -294,7 +301,7 @@ async function handleCommand(command) {
294
301
  await resetConfiguration();
295
302
  break;
296
303
 
297
-
304
+
298
305
 
299
306
  case 'manage':
300
307
  let manageAction;
@@ -319,11 +326,17 @@ async function handleCommand(command) {
319
326
  const config = loadConfig();
320
327
  if (Object.keys(config).length === 0) {
321
328
  console.log('No configuration found. Please run "setup" first.');
322
- return;
329
+ return false;
323
330
  }
331
+ // Set up shutdown handlers to kill server on exit
332
+ setupShutdownHandlers();
324
333
  await launchLemonadeServer(config);
334
+ // After server exits, exit the app completely
335
+ shouldExit = true;
325
336
  break;
326
337
  }
338
+
339
+ return shouldExit;
327
340
  }
328
341
 
329
342
  /**
@@ -334,7 +347,12 @@ async function runCLI() {
334
347
 
335
348
  while (continueRunning) {
336
349
  const command = await showMainMenu();
337
- await handleCommand(command);
350
+ const shouldExit = await handleCommand(command);
351
+
352
+ // Exit the app if we just exited from the server
353
+ if (shouldExit) {
354
+ return;
355
+ }
338
356
 
339
357
  const { continueRunning: shouldContinue } = await inquirer.prompt([
340
358
  {
@@ -349,6 +367,10 @@ async function runCLI() {
349
367
  }
350
368
 
351
369
  console.log('\n👋 Goodbye!\n');
370
+
371
+ // Ensure server is shut down when exiting the CLI
372
+ const { shutdownLemonadeServer } = require('../services/server');
373
+ shutdownLemonadeServer();
352
374
  }
353
375
 
354
376
  module.exports = {
@@ -63,7 +63,7 @@ async function runSetupWizard(isEdit = false) {
63
63
  type: 'confirm',
64
64
  name: 'useCustomModelDir',
65
65
  message: 'Is there another model directory to use? (example: LM Studio)',
66
- default: false
66
+ default: hasExistingModelDir
67
67
  }
68
68
  ]);
69
69
 
@@ -2,14 +2,14 @@ const path = require('path');
2
2
  const os = require('os');
3
3
 
4
4
  // Configuration directories
5
- const USER_CONFIG_DIR = path.join(os.homedir(), '.lemonade-launcher');
5
+ const USER_CONFIG_DIR = path.join(os.homedir(), '.lemonade-interactive-launcher');
6
6
  const USER_CONFIG_FILE = path.join(USER_CONFIG_DIR, 'config.json');
7
- const DEFAULT_LLAMACPP_INSTALL_DIR = path.join(os.homedir(), '.lemonade-launcher', 'llama-cpp');
7
+ const DEFAULT_LLAMACPP_INSTALL_DIR = path.join(os.homedir(), '.lemonade-interactive-launcher', 'llama-cpp');
8
8
 
9
9
  // GitHub API
10
10
  const GITHUB_RELEASES_URL = 'https://api.github.com/repos/ggml-org/llama.cpp/releases';
11
11
  const GITHUB_API_HEADERS = {
12
- 'User-Agent': 'lemonade-launcher',
12
+ 'User-Agent': 'lemonade-interactive-launcher',
13
13
  'Accept': 'application/vnd.github.v3+json'
14
14
  };
15
15
 
@@ -20,7 +20,7 @@ function downloadFile(url, outputPath) {
20
20
 
21
21
  const file = fs.createWriteStream(outputPath);
22
22
 
23
- const req = protocol.get(url, { headers: { 'User-Agent': 'lemonade-launcher' } }, (res) => {
23
+ const req = protocol.get(url, { headers: { 'User-Agent': 'lemonade-interactive-launcher' } }, (res) => {
24
24
  if (res.statusCode === 302 || res.statusCode === 301) {
25
25
  downloadFile(res.headers.location, outputPath)
26
26
  .then(resolve)
@@ -1,9 +1,12 @@
1
1
  const fs = require('fs');
2
- const { execSync } = require('child_process');
2
+ const { execSync, spawn } = require('child_process');
3
3
  const { LEMONADE_SERVER_DEFAULT_PATH } = require('../config/constants');
4
4
  const { findLlamaServer } = require('../utils/system');
5
5
  const { getLlamaServerPath } = require('./asset-manager');
6
6
 
7
+ // Track the server process for graceful shutdown
8
+ let serverProcess = null;
9
+
7
10
  /**
8
11
  * Build server command arguments
9
12
  * @param {Object} config - Server configuration
@@ -154,21 +157,90 @@ async function launchLemonadeServer(config) {
154
157
  console.log('\nStarting server...\n');
155
158
 
156
159
  try {
157
- const command = formatCommand(serverPath, args, {});
158
- execSync(command, {
160
+ // Build the command for spawning
161
+ const commandStr = formatCommand(serverPath, args, {});
162
+
163
+ // Parse the command into executable and arguments
164
+ const parts = commandStr.trim().split(/\s+/);
165
+ const executable = parts[0];
166
+ const execArgs = parts.slice(1);
167
+
168
+ console.log(`Spawning: ${executable} ${execArgs.join(' ')}`);
169
+
170
+ // Spawn the server process
171
+ serverProcess = spawn(executable, execArgs, {
159
172
  stdio: 'inherit',
160
173
  env: process.env
161
174
  });
175
+
176
+ // Wait for the process to exit (blocking)
177
+ await new Promise((resolve, reject) => {
178
+ serverProcess.on('close', (code) => {
179
+ console.log(`\nServer exited with code ${code}`);
180
+ serverProcess = null;
181
+ resolve(code);
182
+ });
183
+
184
+ serverProcess.on('error', (err) => {
185
+ console.error(`Server process error: ${err.message}`);
186
+ serverProcess = null;
187
+ reject(err);
188
+ });
189
+ });
190
+
162
191
  } catch (error) {
163
- console.error(`Server exited with error code: ${error.status}`);
192
+ console.error(`Server exited with error: ${error.message}`);
164
193
  if (error.status !== null) {
165
194
  process.exit(error.status);
166
195
  }
167
196
  }
168
197
  }
169
198
 
199
+ /**
200
+ * Gracefully shutdown the lemonade server
201
+ */
202
+ function shutdownLemonadeServer() {
203
+ if (serverProcess) {
204
+ console.log('\n\nShutting down Lemonade Server...');
205
+
206
+ // Try graceful shutdown first (SIGTERM)
207
+ serverProcess.on('exit', () => {
208
+ console.log('Server shut down successfully.');
209
+ });
210
+
211
+ serverProcess.on('error', (err) => {
212
+ console.error(`Error shutting down server: ${err.message}`);
213
+ });
214
+
215
+ // Send SIGTERM signal
216
+ serverProcess.kill('SIGTERM');
217
+
218
+ // Force kill after 5 seconds if still running
219
+ setTimeout(() => {
220
+ if (serverProcess && !serverProcess.killed) {
221
+ console.log('Force killing server...');
222
+ serverProcess.kill('SIGKILL');
223
+ }
224
+ }, 5000);
225
+ }
226
+ }
227
+
228
+ // Set up signal handlers for graceful shutdown
229
+ function setupShutdownHandlers() {
230
+ const shutdown = (signal) => {
231
+ console.log(`\n\nReceived ${signal}. Shutting down...`);
232
+ shutdownLemonadeServer();
233
+ setTimeout(() => process.exit(0), 2000);
234
+ };
235
+
236
+ process.on('SIGINT', () => shutdown('SIGINT')); // Ctrl+C
237
+ process.on('SIGTERM', () => shutdown('SIGTERM')); // Termination signal
238
+ }
239
+
170
240
  module.exports = {
171
241
  buildServerArgs,
172
242
  formatCommand,
173
- launchLemonadeServer
243
+ launchLemonadeServer,
244
+ shutdownLemonadeServer,
245
+ setupShutdownHandlers
174
246
  };