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.
- package/README.md +288 -0
- package/index.js +16 -0
- package/package.json +40 -0
- package/src/README.md +88 -0
- package/src/cli/menu.js +359 -0
- package/src/cli/prompts.js +247 -0
- package/src/cli/setup-wizard.js +243 -0
- package/src/config/constants.js +83 -0
- package/src/config/index.js +49 -0
- package/src/index.js +58 -0
- package/src/services/asset-manager.js +159 -0
- package/src/services/download.js +102 -0
- package/src/services/github.js +72 -0
- package/src/services/server.js +174 -0
- package/src/utils/system.js +150 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const { GITHUB_RELEASES_URL, GITHUB_API_HEADERS } = require('../config/constants');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fetch all releases from llama.cpp GitHub repository
|
|
7
|
+
* @param {number} limit - Maximum number of releases to fetch
|
|
8
|
+
* @returns {Promise<Array>} Array of release data
|
|
9
|
+
*/
|
|
10
|
+
async function fetchAllReleases(limit = 20) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const options = {
|
|
13
|
+
hostname: 'api.github.com',
|
|
14
|
+
path: `/repos/ggml-org/llama.cpp/releases?per_page=${limit}`,
|
|
15
|
+
method: 'GET',
|
|
16
|
+
headers: GITHUB_API_HEADERS
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const protocol = https;
|
|
20
|
+
const req = protocol.request(options, (res) => {
|
|
21
|
+
let data = '';
|
|
22
|
+
|
|
23
|
+
if (res.statusCode !== 200) {
|
|
24
|
+
reject(new Error(`Request failed with status code ${res.statusCode}`));
|
|
25
|
+
res.resume();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
res.on('data', (chunk) => {
|
|
30
|
+
data += chunk;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
res.on('end', () => {
|
|
34
|
+
try {
|
|
35
|
+
resolve(JSON.parse(data));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
reject(new Error(`Failed to parse response: ${e.message}`));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
req.on('error', (e) => {
|
|
43
|
+
reject(new Error(`Request error: ${e.message}`));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
req.setTimeout(30000, () => {
|
|
47
|
+
req.destroy();
|
|
48
|
+
reject(new Error('Request timed out'));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
req.end();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fetch the latest release from llama.cpp GitHub repository
|
|
57
|
+
* @returns {Promise<Object>} The latest release data
|
|
58
|
+
*/
|
|
59
|
+
async function fetchLatestRelease() {
|
|
60
|
+
const releases = await fetchAllReleases(1);
|
|
61
|
+
|
|
62
|
+
if (releases.length === 0) {
|
|
63
|
+
throw new Error('No releases found');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return releases[0];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
fetchAllReleases,
|
|
71
|
+
fetchLatestRelease
|
|
72
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
const { LEMONADE_SERVER_DEFAULT_PATH } = require('../config/constants');
|
|
4
|
+
const { findLlamaServer } = require('../utils/system');
|
|
5
|
+
const { getLlamaServerPath } = require('./asset-manager');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build server command arguments
|
|
9
|
+
* @param {Object} config - Server configuration
|
|
10
|
+
* @returns {Array} Array of command arguments
|
|
11
|
+
*/
|
|
12
|
+
function buildServerArgs(config) {
|
|
13
|
+
const { host, port, logLevel, modelDir, llamacppArgs } = config;
|
|
14
|
+
const args = [
|
|
15
|
+
'serve',
|
|
16
|
+
'--log-level', logLevel || 'info',
|
|
17
|
+
'--host', host,
|
|
18
|
+
'--port', port.toString()
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
if (modelDir && modelDir !== 'None') {
|
|
22
|
+
args.push('--extra-models-dir', modelDir);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (llamacppArgs) {
|
|
26
|
+
args.push('--llamacpp-args', `"${llamacppArgs}"`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return args;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Format command for cross-platform display
|
|
34
|
+
* @param {string} serverPath - Path to server binary
|
|
35
|
+
* @param {Array} args - Command arguments
|
|
36
|
+
* @param {Object} envVars - Environment variables
|
|
37
|
+
* @returns {string} Formatted command string
|
|
38
|
+
*/
|
|
39
|
+
function formatCommand(serverPath, args, envVars = {}) {
|
|
40
|
+
const isWindows = process.platform === 'win32';
|
|
41
|
+
|
|
42
|
+
if (isWindows) {
|
|
43
|
+
let cmd = '';
|
|
44
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
45
|
+
if (value) {
|
|
46
|
+
cmd += `set ${key}="${value}" && `;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const quotedPath = serverPath.includes(' ') ? `"${serverPath}"` : serverPath;
|
|
51
|
+
const quotedArgs = args.map(arg => arg.includes(' ') ? `"${arg}"` : arg);
|
|
52
|
+
|
|
53
|
+
return cmd + quotedPath + ' ' + quotedArgs.join(' ');
|
|
54
|
+
} else {
|
|
55
|
+
let cmd = '';
|
|
56
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
57
|
+
if (value) {
|
|
58
|
+
cmd += `${key}="${value}" `;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const quotedArgs = args.map(arg => arg.includes(' ') ? `"${arg}"` : arg);
|
|
63
|
+
return cmd + serverPath + ' ' + quotedArgs.join(' ');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Launch lemonade-server with the specified configuration
|
|
69
|
+
* @param {Object} config - Server configuration
|
|
70
|
+
*/
|
|
71
|
+
async function launchLemonadeServer(config) {
|
|
72
|
+
const {
|
|
73
|
+
host,
|
|
74
|
+
port,
|
|
75
|
+
logLevel,
|
|
76
|
+
modelDir,
|
|
77
|
+
llamacppArgs,
|
|
78
|
+
runMode,
|
|
79
|
+
customLlamacppPath,
|
|
80
|
+
customBackendType,
|
|
81
|
+
customServerPath,
|
|
82
|
+
backend
|
|
83
|
+
} = config;
|
|
84
|
+
|
|
85
|
+
console.log('\n=== Launching Lemonade Server ===\n');
|
|
86
|
+
console.log(`Host: ${host}`);
|
|
87
|
+
console.log(`Port: ${port}`);
|
|
88
|
+
console.log(`Log Level: ${logLevel}`);
|
|
89
|
+
console.log(`Backend: ${backend || 'auto'}`);
|
|
90
|
+
console.log(`Model Directory: ${modelDir || 'default'}`);
|
|
91
|
+
console.log(`Run Mode: ${runMode || 'headless'}`);
|
|
92
|
+
if (llamacppArgs) {
|
|
93
|
+
console.log(`llama.cpp Args: ${llamacppArgs}`);
|
|
94
|
+
}
|
|
95
|
+
if (customLlamacppPath) {
|
|
96
|
+
console.log(`Custom llama.cpp: ${customLlamacppPath}`);
|
|
97
|
+
}
|
|
98
|
+
console.log('');
|
|
99
|
+
|
|
100
|
+
const serverPath = LEMONADE_SERVER_DEFAULT_PATH;
|
|
101
|
+
const args = buildServerArgs(config);
|
|
102
|
+
|
|
103
|
+
let backendTypeToUse = customBackendType;
|
|
104
|
+
if (!backendTypeToUse && backend && backend !== 'auto') {
|
|
105
|
+
backendTypeToUse = backend;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let serverBinary = customServerPath;
|
|
109
|
+
if (!serverBinary && customLlamacppPath) {
|
|
110
|
+
serverBinary = findLlamaServer(customLlamacppPath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const envVars = {};
|
|
114
|
+
if (backendTypeToUse && backendTypeToUse !== 'auto' && serverBinary) {
|
|
115
|
+
const backendEnvVar = `LEMONADE_LLAMACPP_${backendTypeToUse.toUpperCase()}_BIN`;
|
|
116
|
+
// Point to the actual binary, not the directory
|
|
117
|
+
const binaryPath = findLlamaServer(customLlamacppPath || serverBinary);
|
|
118
|
+
envVars[backendEnvVar] = binaryPath;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!fs.existsSync(serverPath)) {
|
|
122
|
+
console.error(`\n❌ Error: lemonade-server not found at ${serverPath}`);
|
|
123
|
+
console.log('\n📋 Expected Location:');
|
|
124
|
+
console.log(` ${serverPath}`);
|
|
125
|
+
console.log('\n📥 How to Install Lemonade Server:');
|
|
126
|
+
console.log(' Visit: https://lemonade-server.ai');
|
|
127
|
+
|
|
128
|
+
const command = formatCommand(serverPath, args, envVars);
|
|
129
|
+
console.log('\n🔧 Once installed, this is the command that will be run:');
|
|
130
|
+
console.log(` ${command}`);
|
|
131
|
+
|
|
132
|
+
if (Object.keys(envVars).length > 0) {
|
|
133
|
+
console.log('\n💡 Environment Variable Set:');
|
|
134
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
135
|
+
console.log(` ${key}=${value}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log('\n💡 After installing lemonade-server, run this tool again to start the server.');
|
|
140
|
+
console.log('');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
145
|
+
process.env[key] = value;
|
|
146
|
+
console.log(`Set ${key}=${value}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (Object.keys(envVars).length === 0) {
|
|
150
|
+
console.log('Using default backend configuration');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(`\nCommand: ${serverPath} ${args.join(' ')}`);
|
|
154
|
+
console.log('\nStarting server...\n');
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const command = formatCommand(serverPath, args, {});
|
|
158
|
+
execSync(command, {
|
|
159
|
+
stdio: 'inherit',
|
|
160
|
+
env: process.env
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(`Server exited with error code: ${error.status}`);
|
|
164
|
+
if (error.status !== null) {
|
|
165
|
+
process.exit(error.status);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
buildServerArgs,
|
|
172
|
+
formatCommand,
|
|
173
|
+
launchLemonadeServer
|
|
174
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect user's system for suggested asset
|
|
7
|
+
* @returns {Object} System information
|
|
8
|
+
*/
|
|
9
|
+
function detectSystem() {
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
const arch = process.arch;
|
|
12
|
+
|
|
13
|
+
let osType = 'unknown';
|
|
14
|
+
let archType = arch;
|
|
15
|
+
|
|
16
|
+
if (platform === 'win32') {
|
|
17
|
+
osType = 'windows';
|
|
18
|
+
if (arch === 'x64') archType = 'x64';
|
|
19
|
+
else if (arch === 'arm64') archType = 'arm64';
|
|
20
|
+
else if (arch === 'ia32') archType = 'x86';
|
|
21
|
+
} else if (platform === 'darwin') {
|
|
22
|
+
osType = 'macos';
|
|
23
|
+
if (arch === 'arm64') archType = 'arm64';
|
|
24
|
+
else if (arch === 'x64') archType = 'x64';
|
|
25
|
+
} else if (platform === 'linux') {
|
|
26
|
+
osType = 'linux';
|
|
27
|
+
if (arch === 'x64') archType = 'x64';
|
|
28
|
+
else if (arch === 'arm64') archType = 'arm64';
|
|
29
|
+
else if (arch === 'arm') archType = 'armv7l';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { platform, arch: archType, osType };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Format bytes to human readable string
|
|
37
|
+
* @param {number} bytes - Bytes to format
|
|
38
|
+
* @returns {string} Formatted byte string
|
|
39
|
+
*/
|
|
40
|
+
function formatBytes(bytes) {
|
|
41
|
+
if (bytes === 0) return '0 B';
|
|
42
|
+
const k = 1024;
|
|
43
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
44
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
45
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Infer backend type from asset name
|
|
50
|
+
* @param {string} assetName - Asset filename
|
|
51
|
+
* @returns {string} Backend type
|
|
52
|
+
*/
|
|
53
|
+
function inferBackendType(assetName) {
|
|
54
|
+
const name = assetName.toLowerCase();
|
|
55
|
+
|
|
56
|
+
if (name.includes('rocm') || name.includes('hip')) return 'rocm';
|
|
57
|
+
if (name.includes('vulkan')) return 'vulkan';
|
|
58
|
+
if (name.includes('cuda')) return 'cuda';
|
|
59
|
+
if (name.includes('sycl')) return 'sycl';
|
|
60
|
+
if (name.includes('opencl')) return 'opencl';
|
|
61
|
+
if (name.includes('cpu')) return 'cpu';
|
|
62
|
+
|
|
63
|
+
return 'cpu';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Categorize asset based on its name
|
|
68
|
+
* @param {string} assetName - Asset filename
|
|
69
|
+
* @returns {string} Category
|
|
70
|
+
*/
|
|
71
|
+
function categorizeAsset(assetName) {
|
|
72
|
+
const name = assetName.toLowerCase();
|
|
73
|
+
|
|
74
|
+
if (name.includes('cuda')) return 'CUDA';
|
|
75
|
+
if (name.includes('rocm') || name.includes('hip')) return 'ROCm';
|
|
76
|
+
if (name.includes('vulkan')) return 'Vulkan';
|
|
77
|
+
if (name.includes('sycl')) return 'SYCL';
|
|
78
|
+
if (name.includes('opencl')) return 'OpenCL';
|
|
79
|
+
if (name.includes('cpu')) return 'CPU';
|
|
80
|
+
if (name.includes('macos') || name.includes('xcframework')) return 'macOS';
|
|
81
|
+
if (name.includes('ubuntu') || name.includes('linux')) return 'Linux';
|
|
82
|
+
if (name.includes('win') || name.includes('windows')) return 'Windows';
|
|
83
|
+
|
|
84
|
+
return 'Other';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Determine asset type (zip or tar.gz)
|
|
89
|
+
* @param {string} assetName - Asset filename
|
|
90
|
+
* @returns {string} 'zip', 'tar', or 'unknown'
|
|
91
|
+
*/
|
|
92
|
+
function getAssetType(assetName) {
|
|
93
|
+
if (assetName.endsWith('.zip')) return 'zip';
|
|
94
|
+
if (assetName.endsWith('.tar.gz')) return 'tar';
|
|
95
|
+
return 'unknown';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Filter assets for llama-server binaries only
|
|
100
|
+
* @param {Array} assets - Array of asset objects
|
|
101
|
+
* @returns {Array} Filtered assets
|
|
102
|
+
*/
|
|
103
|
+
function filterServerAssets(assets) {
|
|
104
|
+
return assets.filter(asset => {
|
|
105
|
+
const name = asset.name.toLowerCase();
|
|
106
|
+
return name.includes('bin') && (name.endsWith('.zip') || name.endsWith('.tar.gz'));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Find the llama-server binary in extracted directory
|
|
112
|
+
* @param {string} extractDir - Directory to search
|
|
113
|
+
* @returns {string|null} Path to llama-server or null
|
|
114
|
+
*/
|
|
115
|
+
function findLlamaServer(extractDir) {
|
|
116
|
+
const candidates = process.platform === 'win32'
|
|
117
|
+
? ['llama-server.exe']
|
|
118
|
+
: ['llama-server'];
|
|
119
|
+
|
|
120
|
+
function searchDir(dir) {
|
|
121
|
+
if (!fs.existsSync(dir)) return null;
|
|
122
|
+
|
|
123
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
124
|
+
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
const fullPath = path.join(dir, entry.name);
|
|
127
|
+
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
const result = searchDir(fullPath);
|
|
130
|
+
if (result) return result;
|
|
131
|
+
} else if (entry.isFile() && candidates.includes(entry.name)) {
|
|
132
|
+
return fullPath;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return searchDir(extractDir);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
detectSystem,
|
|
144
|
+
formatBytes,
|
|
145
|
+
inferBackendType,
|
|
146
|
+
categorizeAsset,
|
|
147
|
+
getAssetType,
|
|
148
|
+
filterServerAssets,
|
|
149
|
+
findLlamaServer
|
|
150
|
+
};
|