UNPKG

electron-ollama

Version:

Bundle Ollama with your Electron.js app for seamless user experience

284 lines 11.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ElectronOllama = exports.ElectronOllamaServer = void 0; const path = __importStar(require("path")); const fs = __importStar(require("fs/promises")); const fs_1 = require("fs"); const os = __importStar(require("os")); const github_fetch_1 = require("./github-fetch"); const unzip_1 = require("./unzip"); const untgz_1 = require("./untgz"); const server_1 = require("./server"); Object.defineProperty(exports, "ElectronOllamaServer", { enumerable: true, get: function () { return server_1.ElectronOllamaServer; } }); const stream_1 = require("stream"); const promises_1 = require("stream/promises"); class ElectronOllama { constructor(config) { this.server = null; this.config = { directory: 'electron-ollama', ...config, }; } /** * Get the current platform configuration */ currentPlatformConfig() { const platform = os.platform(); const arch = os.arch(); let osType; let architecture; // Map platform switch (platform) { case 'win32': osType = 'windows'; break; case 'darwin': osType = 'darwin'; break; case 'linux': osType = 'linux'; break; default: throw new Error(`Unsupported platform: ${platform}`); } // Map architecture switch (arch) { case 'arm64': architecture = 'arm64'; break; case 'x64': architecture = 'amd64'; break; default: throw new Error(`Unsupported architecture: ${arch}`); } return { os: osType, arch: architecture, }; } /** * Get the name of the asset for the given platform configuration (e.g. "ollama-windows-amd64.zip" or "ollama-darwin.tgz") */ getAssetName(platformConfig) { const { os, arch: architecture } = platformConfig; switch (os) { case 'windows': return `ollama-windows-${architecture}.zip`; case 'darwin': return 'ollama-darwin.tgz'; case 'linux': return `ollama-linux-${architecture}.tgz`; } } /** * Get metadata for a specific version ('latest' by default) and platform */ async getMetadata(version = 'latest', platformConfig = this.currentPlatformConfig()) { const { os, arch: architecture } = platformConfig; const releaseUrlPath = version === 'latest' ? `latest` : `tags/${version}`; const gitHubResponse = await (0, github_fetch_1.githubFetch)(`https://api.github.com/repos/ollama/ollama/releases/${releaseUrlPath}`); const releaseData = await gitHubResponse.json(); const assetName = this.getAssetName(platformConfig); const asset = releaseData.assets.find((asset) => asset.name === assetName); if (!asset) { throw new Error(`${os}-${architecture} is not supported by Ollama ${releaseData.tag_name}`); } return { digest: asset.digest, size: asset.size, sizeMB: (asset.size / 1024 / 1024).toFixed(1), fileName: asset.name, contentType: asset.content_type, version: releaseData.tag_name, downloads: asset.download_count, downloadUrl: asset.browser_download_url, releaseUrl: releaseData.html_url, body: releaseData.body, }; } /** * Download Ollama for the specified version ('latest' by default) and platform */ async download(version = 'latest', platformConfig = this.currentPlatformConfig(), { log } = {}) { const metadata = await this.getMetadata(version, platformConfig); const versionDir = this.getBinPath(metadata.version, platformConfig); // 1. Create directory if it doesn't exist log?.(0, 'Creating directory'); await fs.mkdir(versionDir, { recursive: true }); // 2. Download the file log?.(0, `Downloading ${metadata.fileName} (${metadata.sizeMB}MB)`); const response = await fetch(metadata.downloadUrl); // Create a progress-tracking transform stream that works with Web API streams let downloadedBytes = 0; const totalBytes = metadata.size; // this is estimate from metadata let lastLoggedPercent = 0; const progressStream = new stream_1.Transform({ transform(chunk, _encoding, callback) { downloadedBytes += chunk.length; // Log progress in 1% increments const currentPercent = Math.floor((downloadedBytes / totalBytes) * 100); if (currentPercent > lastLoggedPercent) { if (currentPercent < 100) { log?.(currentPercent, `Downloading ${metadata.fileName} (${(downloadedBytes / 1024 / 1024).toFixed(1)}MB / ${metadata.sizeMB}MB) ${currentPercent}%`); } else { log?.(100, `Extracting ${metadata.fileName} (${metadata.sizeMB}MB)`); } lastLoggedPercent = currentPercent; } // Pass the chunk through unchanged callback(null, chunk); } }); // Convert Web API ReadableStream to Node.js stream using Readable.fromWeb() if (!response.body) { throw new Error('Response body is not readable'); } const nodeStream = stream_1.Readable.fromWeb(response.body); nodeStream.pipe(progressStream); // 3. Extract the archive if (metadata.contentType === 'application/zip') { // For zip files, stream directly to file then extract const filePath = path.join(versionDir, metadata.fileName); const writeStream = (0, fs_1.createWriteStream)(filePath); // Use pipeline to handle the entire stream chain with automatic promise handling await (0, promises_1.pipeline)(progressStream, writeStream); // Now extract the downloaded file await (0, unzip_1.unzipFile)(filePath, versionDir, true); } else if (['application/x-gtar', 'application/x-tar', 'application/x-gzip', 'application/tar', 'application/gzip', 'application/x-tgz'].includes(metadata.contentType)) { // For tar archives, stream directly to extraction await (0, untgz_1.untgzStream)(progressStream, versionDir); } else { throw new Error(`The Ollama asset type ${metadata.contentType} is not supported`); } log?.(100, `Extracted archive ${metadata.fileName}`); // 4. Verify checksum } /** * Check if a version is downloaded for the given platform configuration */ async isDownloaded(version, platformConfig = this.currentPlatformConfig()) { const binPath = this.getBinPath(version, platformConfig); const executableName = this.getExecutableName(platformConfig); return fs.access(path.join(binPath, executableName)).then(() => true).catch(() => false); } /** * List all downloaded versions for the given platform configuration */ async downloadedVersions(platformConfig = this.currentPlatformConfig()) { let versions = []; try { versions = await fs.readdir(path.join(this.config.basePath, this.config.directory)); } catch { return []; // directory does not exist - nothing to list } const downloaded = await Promise.all(versions.map((version) => this.isDownloaded(version, platformConfig))); return versions.filter((_version, index) => downloaded[index]); } /** * Get the path to the directory for the given version and platform configuration */ getBinPath(version, platformConfig = this.currentPlatformConfig()) { return path.join(this.config.basePath, this.config.directory, version, platformConfig.os, platformConfig.arch); } /** * Get the name of the executable for the given platform configuration */ getExecutableName(platformConfig) { switch (platformConfig.os) { case 'windows': return 'ollama.exe'; case 'darwin': return 'ollama'; case 'linux': return 'bin/ollama'; } } /** * Start serving Ollama with the specified version and wait until it is running */ async serve(version, { serverLog, downloadLog, timeoutSec = 5 } = {}) { const platformConfig = this.currentPlatformConfig(); const binPath = this.getBinPath(version, platformConfig); const intervalMs = 100; const intervalCount = Math.ceil(timeoutSec * 1000 / intervalMs); // Ensure the binary exists if (!await this.isDownloaded(version, platformConfig)) { await this.download(version, platformConfig, { log: downloadLog || (() => { }) }); } this.server = new server_1.ElectronOllamaServer({ binPath, log: serverLog || (() => { }), }); this.server.start(this.getExecutableName(platformConfig)); // Wait for the server to start in 100ms intervals for (let i = 0; i < intervalCount; i++) { await new Promise(resolve => setTimeout(resolve, intervalMs)); if (await this.isRunning()) { return; } } throw new Error(`Ollama server failed to start in ${timeoutSec}s`); } /** * Get the server instance started by serve() */ getServer() { return this.server || null; } /** * Check if Ollama is running */ async isRunning() { try { const response = await fetch('http://localhost:11434'); const text = await response.text(); return text.includes('Ollama is running'); } catch { return false; } } } exports.ElectronOllama = ElectronOllama; // Export default instance exports.default = ElectronOllama; //# sourceMappingURL=index.js.map