mrmd-server 0.2.6 → 0.2.7
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/package.json +1 -2
- package/src/services.js +10 -10
- package/src/sync-manager.js +3 -3
- package/src/vendor/config.js +295 -0
- package/src/vendor/services/asset-service.js +332 -0
- package/src/vendor/services/file-service.js +520 -0
- package/src/vendor/services/languagetool-preferences-service.js +341 -0
- package/src/vendor/services/languagetool-service.js +763 -0
- package/src/vendor/services/project-service.js +439 -0
- package/src/vendor/services/runtime-preferences-service.js +603 -0
- package/src/vendor/services/runtime-service.js +1075 -0
- package/src/vendor/services/settings-service.js +715 -0
- package/src/vendor/utils/index.js +8 -0
- package/src/vendor/utils/network.js +116 -0
- package/src/vendor/utils/platform.js +472 -0
- package/src/vendor/utils/python.js +309 -0
- package/src/vendor/utils/uv-installer.js +299 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python environment utilities for mrmd-electron
|
|
3
|
+
*
|
|
4
|
+
* Shared functions for Python/venv management used by main process and services.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn, execSync } from 'child_process';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { PYTHON_DEPS, getPythonInstallArgs } from '../config.js';
|
|
13
|
+
import { findUv, ensureUv } from './uv-installer.js';
|
|
14
|
+
import {
|
|
15
|
+
getVenvPython,
|
|
16
|
+
getVenvExecutable,
|
|
17
|
+
getVenvBinDir,
|
|
18
|
+
getPythonCommand,
|
|
19
|
+
} from './platform.js';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
|
|
24
|
+
function resolveLocalMrmdPythonSource() {
|
|
25
|
+
const explicit = process.env.MRMD_PYTHON_DEV;
|
|
26
|
+
if (explicit && fs.existsSync(path.join(explicit, 'pyproject.toml'))) {
|
|
27
|
+
return { path: explicit, editable: true, source: 'env' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const sibling = path.resolve(__dirname, '../../../mrmd-python');
|
|
32
|
+
if (fs.existsSync(path.join(sibling, 'pyproject.toml'))) {
|
|
33
|
+
return { path: sibling, editable: true, source: 'sibling' };
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
if (process.resourcesPath) {
|
|
41
|
+
const bundled = path.join(process.resourcesPath, 'mrmd-python');
|
|
42
|
+
if (fs.existsSync(path.join(bundled, 'pyproject.toml'))) {
|
|
43
|
+
return { path: bundled, editable: false, source: 'bundled' };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Re-export for backwards compatibility
|
|
54
|
+
export { findUv, ensureUv };
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Find uv binary (sync version for use in constructors)
|
|
58
|
+
* @deprecated Use findUv from uv-installer.js instead
|
|
59
|
+
* @returns {string|null} Path to uv or null if not found
|
|
60
|
+
*/
|
|
61
|
+
export function findUvSync() {
|
|
62
|
+
return findUv();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Install all required Python packages in a virtual environment
|
|
67
|
+
*
|
|
68
|
+
* Uses uv (auto-installed if missing). Installs packages according to
|
|
69
|
+
* the version compatibility matrix in config.js.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} venvPath - Path to the virtual environment
|
|
72
|
+
* @param {object} options - Options
|
|
73
|
+
* @param {string} options.localDev - Local development path (from MRMD_PYTHON_DEV env)
|
|
74
|
+
* @param {boolean} options.fullInstall - Install all packages including optional (default: true)
|
|
75
|
+
* @param {function} options.onProgress - Progress callback (stage, detail)
|
|
76
|
+
* @returns {Promise<{ success: boolean, packages: string[] }>}
|
|
77
|
+
*/
|
|
78
|
+
export async function installMrmdPython(venvPath, options = {}) {
|
|
79
|
+
const {
|
|
80
|
+
localDev = process.env.MRMD_PYTHON_DEV,
|
|
81
|
+
fullInstall = true,
|
|
82
|
+
onProgress
|
|
83
|
+
} = options;
|
|
84
|
+
|
|
85
|
+
const localSource = localDev
|
|
86
|
+
? { path: localDev, editable: true, source: 'explicit' }
|
|
87
|
+
: resolveLocalMrmdPythonSource();
|
|
88
|
+
|
|
89
|
+
const report = (stage, detail) => {
|
|
90
|
+
console.log(`[python] ${stage}: ${detail}`);
|
|
91
|
+
if (onProgress) onProgress(stage, detail);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const pythonPath = getVenvPython(venvPath);
|
|
95
|
+
|
|
96
|
+
// Validate venv exists
|
|
97
|
+
if (!fs.existsSync(pythonPath)) {
|
|
98
|
+
throw new Error(`Python not found at ${pythonPath}. Is this a valid venv?`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Ensure uv is installed (auto-install if missing)
|
|
102
|
+
report('checking', 'uv installation');
|
|
103
|
+
const uvPath = await ensureUv({
|
|
104
|
+
onProgress: (stage, detail) => report(`uv-${stage}`, detail)
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Build package list from version matrix
|
|
108
|
+
const packages = [];
|
|
109
|
+
|
|
110
|
+
if (localSource?.path) {
|
|
111
|
+
// Prefer bundled/local mrmd-python source so packaged apps don't depend on
|
|
112
|
+
// PyPI for MRMD's own runtime package. Keep third-party deps from PyPI.
|
|
113
|
+
const installArgs = getPythonInstallArgs().filter((spec) => !spec.startsWith('mrmd-python'));
|
|
114
|
+
packages.push(...installArgs);
|
|
115
|
+
if (localSource.editable) {
|
|
116
|
+
packages.push('-e', localSource.path);
|
|
117
|
+
} else {
|
|
118
|
+
packages.push(localSource.path);
|
|
119
|
+
}
|
|
120
|
+
report('mode', `${localSource.source} mrmd-python + PyPI deps (${localSource.path})`);
|
|
121
|
+
} else {
|
|
122
|
+
// Production: install from PyPI with version constraints
|
|
123
|
+
const installArgs = getPythonInstallArgs();
|
|
124
|
+
packages.push(...installArgs);
|
|
125
|
+
report('mode', `PyPI (${installArgs.length} packages)`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Run uv pip install
|
|
129
|
+
const args = ['pip', 'install', '--python', pythonPath, ...packages];
|
|
130
|
+
|
|
131
|
+
report('installing', packages.filter(p => !p.startsWith('-')).join(', '));
|
|
132
|
+
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
console.log(`[python] Running: ${uvPath} ${args.join(' ')}`);
|
|
135
|
+
|
|
136
|
+
const proc = spawn(uvPath, args, {
|
|
137
|
+
cwd: path.dirname(venvPath),
|
|
138
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
139
|
+
env: { ...process.env, VIRTUAL_ENV: venvPath },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
let stdout = '';
|
|
143
|
+
let stderr = '';
|
|
144
|
+
|
|
145
|
+
proc.stdout.on('data', (d) => {
|
|
146
|
+
stdout += d.toString();
|
|
147
|
+
const line = d.toString().trim();
|
|
148
|
+
if (line) console.log('[uv]', line);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
proc.stderr.on('data', (d) => {
|
|
152
|
+
stderr += d.toString();
|
|
153
|
+
const line = d.toString().trim();
|
|
154
|
+
if (line) console.error('[uv]', line);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
proc.on('error', (e) => {
|
|
158
|
+
reject(new Error(`Failed to run uv: ${e.message}`));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
proc.on('close', (code) => {
|
|
162
|
+
if (code === 0) {
|
|
163
|
+
report('complete', 'all packages installed');
|
|
164
|
+
resolve({
|
|
165
|
+
success: true,
|
|
166
|
+
packages: Object.keys(PYTHON_DEPS)
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
reject(new Error(`uv pip install failed (code ${code}): ${(stderr || stdout).slice(-500)}`));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Install mrmd-python only (legacy function for backwards compatibility)
|
|
177
|
+
* @deprecated Use installMrmdPython with fullInstall option
|
|
178
|
+
*/
|
|
179
|
+
export async function installMrmdPythonOnly(venvPath, options = {}) {
|
|
180
|
+
return installMrmdPython(venvPath, { ...options, fullInstall: false });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create a Python virtual environment
|
|
185
|
+
*
|
|
186
|
+
* Tries uv first (faster), falls back to python -m venv.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} venvPath - Path for the new venv
|
|
189
|
+
* @returns {Promise<void>}
|
|
190
|
+
*/
|
|
191
|
+
export function createVenv(venvPath) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const uvPath = findUvSync();
|
|
194
|
+
|
|
195
|
+
if (uvPath) {
|
|
196
|
+
// Try uv first (faster)
|
|
197
|
+
const proc = spawn(uvPath, ['venv', venvPath], {
|
|
198
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
let stderr = '';
|
|
202
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
203
|
+
|
|
204
|
+
proc.on('close', (code) => {
|
|
205
|
+
if (code === 0) {
|
|
206
|
+
resolve();
|
|
207
|
+
} else {
|
|
208
|
+
reject(new Error(`uv venv failed: ${stderr}`));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
proc.on('error', () => {
|
|
213
|
+
// uv failed, fall back to python -m venv
|
|
214
|
+
createVenvWithPython(venvPath).then(resolve).catch(reject);
|
|
215
|
+
});
|
|
216
|
+
} else {
|
|
217
|
+
// No uv, use python -m venv
|
|
218
|
+
createVenvWithPython(venvPath).then(resolve).catch(reject);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create venv using python -m venv
|
|
225
|
+
*/
|
|
226
|
+
function createVenvWithPython(venvPath) {
|
|
227
|
+
return new Promise((resolve, reject) => {
|
|
228
|
+
const pythonCmd = getPythonCommand();
|
|
229
|
+
const proc = spawn(pythonCmd, ['-m', 'venv', venvPath], {
|
|
230
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
let stderr = '';
|
|
234
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
235
|
+
|
|
236
|
+
proc.on('error', (e) => {
|
|
237
|
+
reject(new Error(`Failed to create venv: ${e.message}`));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
proc.on('close', (code) => {
|
|
241
|
+
if (code === 0) {
|
|
242
|
+
resolve();
|
|
243
|
+
} else {
|
|
244
|
+
reject(new Error(`python -m venv failed (code ${code}): ${stderr}`));
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get information about a Python environment
|
|
252
|
+
*
|
|
253
|
+
* @param {string} envPath - Path to the environment
|
|
254
|
+
* @param {string} envType - Type of environment ('system', 'venv', 'conda', 'pyenv')
|
|
255
|
+
* @returns {object} Environment info
|
|
256
|
+
*/
|
|
257
|
+
export function getEnvInfo(envPath, envType) {
|
|
258
|
+
let pythonVersion = null;
|
|
259
|
+
let hasMrmdPython = false;
|
|
260
|
+
let hasPython = false;
|
|
261
|
+
let projectName = '';
|
|
262
|
+
let name = '';
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
hasPython = fs.existsSync(getVenvPython(envPath));
|
|
266
|
+
|
|
267
|
+
// Get Python version from pyvenv.cfg
|
|
268
|
+
const pyvenvCfg = path.join(envPath, 'pyvenv.cfg');
|
|
269
|
+
if (fs.existsSync(pyvenvCfg)) {
|
|
270
|
+
const content = fs.readFileSync(pyvenvCfg, 'utf8');
|
|
271
|
+
const match = content.match(/version\s*=\s*(\d+\.\d+)/);
|
|
272
|
+
if (match) pythonVersion = match[1];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check if mrmd-python is installed
|
|
276
|
+
hasMrmdPython = fs.existsSync(getVenvExecutable(envPath, 'mrmd-python'));
|
|
277
|
+
|
|
278
|
+
// Set name based on environment type
|
|
279
|
+
switch (envType) {
|
|
280
|
+
case 'system':
|
|
281
|
+
name = 'System Python';
|
|
282
|
+
projectName = 'system';
|
|
283
|
+
break;
|
|
284
|
+
case 'conda':
|
|
285
|
+
name = path.basename(envPath);
|
|
286
|
+
projectName = 'conda';
|
|
287
|
+
break;
|
|
288
|
+
case 'pyenv':
|
|
289
|
+
name = path.basename(envPath);
|
|
290
|
+
projectName = 'pyenv';
|
|
291
|
+
break;
|
|
292
|
+
default: // venv
|
|
293
|
+
name = path.basename(envPath);
|
|
294
|
+
projectName = path.basename(path.dirname(envPath));
|
|
295
|
+
}
|
|
296
|
+
} catch (e) {
|
|
297
|
+
console.warn(`[python] Error getting env info for ${envPath}:`, e.message);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
path: envPath,
|
|
302
|
+
pythonVersion,
|
|
303
|
+
hasMrmdPython,
|
|
304
|
+
hasPython,
|
|
305
|
+
projectName,
|
|
306
|
+
name,
|
|
307
|
+
envType,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UV Auto-Installer for mrmd-electron
|
|
3
|
+
*
|
|
4
|
+
* Automatically downloads and installs uv if not present.
|
|
5
|
+
* uv is required for fast Python package management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn, execSync } from 'child_process';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import https from 'https';
|
|
13
|
+
import { createWriteStream, createReadStream } from 'fs';
|
|
14
|
+
import { pipeline } from 'stream/promises';
|
|
15
|
+
import { createGunzip } from 'zlib';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
UV_PATHS,
|
|
19
|
+
UV_DOWNLOAD_URLS,
|
|
20
|
+
UV_INSTALL_DIR,
|
|
21
|
+
UV_INSTALL_PATH,
|
|
22
|
+
} from '../config.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find existing uv installation
|
|
26
|
+
* @returns {string|null} Path to uv binary or null
|
|
27
|
+
*/
|
|
28
|
+
export function findUv() {
|
|
29
|
+
// Check configured paths
|
|
30
|
+
for (const loc of UV_PATHS) {
|
|
31
|
+
if (fs.existsSync(loc)) {
|
|
32
|
+
return loc;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check our install location
|
|
37
|
+
if (fs.existsSync(UV_INSTALL_PATH)) {
|
|
38
|
+
return UV_INSTALL_PATH;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Try PATH via 'which' (Unix) or 'where' (Windows)
|
|
42
|
+
try {
|
|
43
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
44
|
+
const result = execSync(`${cmd} uv`, {
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
47
|
+
}).trim().split('\n')[0];
|
|
48
|
+
|
|
49
|
+
if (result && fs.existsSync(result)) {
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// uv not in PATH
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the download URL for current platform
|
|
61
|
+
* @returns {string|null} Download URL or null if unsupported
|
|
62
|
+
*/
|
|
63
|
+
function getDownloadUrl() {
|
|
64
|
+
const platform = process.platform;
|
|
65
|
+
const arch = process.arch;
|
|
66
|
+
|
|
67
|
+
const key = `${platform}-${arch}`;
|
|
68
|
+
return UV_DOWNLOAD_URLS[key] || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Download a file to a local path
|
|
73
|
+
* @param {string} url - URL to download
|
|
74
|
+
* @param {string} dest - Destination path
|
|
75
|
+
* @param {function} onProgress - Progress callback (received, total)
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
*/
|
|
78
|
+
function downloadFile(url, dest, onProgress) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const file = createWriteStream(dest);
|
|
81
|
+
|
|
82
|
+
const request = (url) => {
|
|
83
|
+
https.get(url, (response) => {
|
|
84
|
+
// Handle redirects
|
|
85
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
86
|
+
request(response.headers.location);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (response.statusCode !== 200) {
|
|
91
|
+
reject(new Error(`Download failed: HTTP ${response.statusCode}`));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const total = parseInt(response.headers['content-length'], 10);
|
|
96
|
+
let received = 0;
|
|
97
|
+
|
|
98
|
+
response.on('data', (chunk) => {
|
|
99
|
+
received += chunk.length;
|
|
100
|
+
if (onProgress) onProgress(received, total);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
response.pipe(file);
|
|
104
|
+
|
|
105
|
+
file.on('finish', () => {
|
|
106
|
+
file.close();
|
|
107
|
+
resolve();
|
|
108
|
+
});
|
|
109
|
+
}).on('error', (err) => {
|
|
110
|
+
fs.unlink(dest, () => {});
|
|
111
|
+
reject(err);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
request(url);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract tar.gz archive
|
|
121
|
+
* @param {string} archive - Path to archive
|
|
122
|
+
* @param {string} dest - Destination directory
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
*/
|
|
125
|
+
async function extractTarGz(archive, dest) {
|
|
126
|
+
// Use tar command (available on Unix and modern Windows)
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const proc = spawn('tar', ['-xzf', archive, '-C', dest], {
|
|
129
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
let stderr = '';
|
|
133
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
134
|
+
|
|
135
|
+
proc.on('close', (code) => {
|
|
136
|
+
if (code === 0) resolve();
|
|
137
|
+
else reject(new Error(`tar extract failed: ${stderr}`));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
proc.on('error', reject);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Extract zip archive (Windows)
|
|
146
|
+
* @param {string} archive - Path to archive
|
|
147
|
+
* @param {string} dest - Destination directory
|
|
148
|
+
* @returns {Promise<void>}
|
|
149
|
+
*/
|
|
150
|
+
async function extractZip(archive, dest) {
|
|
151
|
+
// Use PowerShell on Windows
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const proc = spawn('powershell', [
|
|
154
|
+
'-Command',
|
|
155
|
+
`Expand-Archive -Path '${archive}' -DestinationPath '${dest}' -Force`
|
|
156
|
+
], {
|
|
157
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
let stderr = '';
|
|
161
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
162
|
+
|
|
163
|
+
proc.on('close', (code) => {
|
|
164
|
+
if (code === 0) resolve();
|
|
165
|
+
else reject(new Error(`zip extract failed: ${stderr}`));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
proc.on('error', reject);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Install uv automatically
|
|
174
|
+
* @param {object} options - Options
|
|
175
|
+
* @param {function} options.onProgress - Progress callback (stage, detail)
|
|
176
|
+
* @returns {Promise<string>} Path to installed uv binary
|
|
177
|
+
*/
|
|
178
|
+
export async function installUv(options = {}) {
|
|
179
|
+
const { onProgress } = options;
|
|
180
|
+
|
|
181
|
+
const report = (stage, detail) => {
|
|
182
|
+
console.log(`[uv-install] ${stage}: ${detail}`);
|
|
183
|
+
if (onProgress) onProgress(stage, detail);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Check if already installed
|
|
187
|
+
const existing = findUv();
|
|
188
|
+
if (existing) {
|
|
189
|
+
report('found', existing);
|
|
190
|
+
return existing;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Get download URL
|
|
194
|
+
const url = getDownloadUrl();
|
|
195
|
+
if (!url) {
|
|
196
|
+
throw new Error(`Unsupported platform: ${process.platform}-${process.arch}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
report('downloading', url);
|
|
200
|
+
|
|
201
|
+
// Create temp directory
|
|
202
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'uv-install-'));
|
|
203
|
+
const isZip = url.endsWith('.zip');
|
|
204
|
+
const archivePath = path.join(tmpDir, isZip ? 'uv.zip' : 'uv.tar.gz');
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// Download
|
|
208
|
+
await downloadFile(url, archivePath, (received, total) => {
|
|
209
|
+
const pct = total ? Math.round((received / total) * 100) : 0;
|
|
210
|
+
report('downloading', `${pct}%`);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
report('extracting', archivePath);
|
|
214
|
+
|
|
215
|
+
// Extract
|
|
216
|
+
if (isZip) {
|
|
217
|
+
await extractZip(archivePath, tmpDir);
|
|
218
|
+
} else {
|
|
219
|
+
await extractTarGz(archivePath, tmpDir);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Find the uv binary in extracted files
|
|
223
|
+
const uvBinary = process.platform === 'win32' ? 'uv.exe' : 'uv';
|
|
224
|
+
let extractedUv = null;
|
|
225
|
+
|
|
226
|
+
// Search for uv in tmpDir (might be in a subdirectory)
|
|
227
|
+
const searchDir = (dir) => {
|
|
228
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
229
|
+
const fullPath = path.join(dir, entry.name);
|
|
230
|
+
if (entry.isDirectory()) {
|
|
231
|
+
const found = searchDir(fullPath);
|
|
232
|
+
if (found) return found;
|
|
233
|
+
} else if (entry.name === uvBinary) {
|
|
234
|
+
return fullPath;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
extractedUv = searchDir(tmpDir);
|
|
241
|
+
|
|
242
|
+
if (!extractedUv) {
|
|
243
|
+
throw new Error('uv binary not found in archive');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
report('installing', UV_INSTALL_PATH);
|
|
247
|
+
|
|
248
|
+
// Ensure install directory exists
|
|
249
|
+
fs.mkdirSync(UV_INSTALL_DIR, { recursive: true });
|
|
250
|
+
|
|
251
|
+
// Move uv to install location
|
|
252
|
+
fs.copyFileSync(extractedUv, UV_INSTALL_PATH);
|
|
253
|
+
|
|
254
|
+
// Make executable (Unix)
|
|
255
|
+
if (process.platform !== 'win32') {
|
|
256
|
+
fs.chmodSync(UV_INSTALL_PATH, 0o755);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
report('complete', UV_INSTALL_PATH);
|
|
260
|
+
|
|
261
|
+
return UV_INSTALL_PATH;
|
|
262
|
+
|
|
263
|
+
} finally {
|
|
264
|
+
// Cleanup temp directory
|
|
265
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Ensure uv is installed, installing if necessary
|
|
271
|
+
* @param {object} options - Options passed to installUv
|
|
272
|
+
* @returns {Promise<string>} Path to uv binary
|
|
273
|
+
*/
|
|
274
|
+
export async function ensureUv(options = {}) {
|
|
275
|
+
const existing = findUv();
|
|
276
|
+
if (existing) {
|
|
277
|
+
return existing;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return installUv(options);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get uv version
|
|
285
|
+
* @param {string} uvPath - Path to uv binary
|
|
286
|
+
* @returns {string|null} Version string or null
|
|
287
|
+
*/
|
|
288
|
+
export function getUvVersion(uvPath) {
|
|
289
|
+
try {
|
|
290
|
+
const result = execSync(`"${uvPath}" --version`, {
|
|
291
|
+
encoding: 'utf8',
|
|
292
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
293
|
+
}).trim();
|
|
294
|
+
// Output is like "uv 0.5.14"
|
|
295
|
+
return result.split(' ')[1] || result;
|
|
296
|
+
} catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|