diff --git a/package.json b/package.json index d947469..880526f 100644 --- a/package.json +++ b/package.json @@ -28,37 +28,23 @@ "@arethetypeswrong/cli": "^0.17.0", "@swc/core": "^1.3.102", "@swc/jest": "^0.2.29", - "@types/archiver": "^6.0.3", - "@types/bun": "^1.2.12", - "@types/fs-extra": "^11.0.4", - "@types/ignore-walk": "^4.0.3", "@types/jest": "^29.4.0", "@types/node": "^20.17.6", - "@types/tmp": "^0.2.6", + "typescript-eslint": "8.31.1", "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", - "archiver": "^7.0.1", - "chalk": "^5.4.1", "eslint": "^9.20.1", "eslint-plugin-prettier": "^5.2.3", "eslint-plugin-unused-imports": "^4.1.4", - "execa": "^9.5.3", - "fs-extra": "^11.3.0", - "get-port": "^7.1.0", "iconv-lite": "^0.6.3", - "ignore-walk": "^7.0.0", "jest": "^29.4.0", "prettier": "^3.0.0", "publint": "^0.2.12", - "smol-toml": "^1.3.4", - "tar": "^7.4.3", - "tmp": "^0.2.3", "ts-jest": "^29.1.0", "ts-node": "^10.5.0", "tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.4/tsc-multi-1.1.4.tgz", "tsconfig-paths": "^4.0.0", - "typescript": "5.8.3", - "typescript-eslint": "8.31.1" + "typescript": "5.8.3" }, "resolutions": { "synckit": "0.8.8" diff --git a/src/cli/index.ts b/src/cli/index.ts deleted file mode 100755 index cb426cb..0000000 --- a/src/cli/index.ts +++ /dev/null @@ -1,423 +0,0 @@ -#!/usr/bin/env bun -import chalk from 'chalk'; -import { Command } from 'commander'; -import fs, { createReadStream } from 'fs'; -import getPort from 'get-port'; -import os from 'os'; -import path from 'path'; -import * as tmp from 'tmp'; -import type { KernelJson } from '../core/app-framework'; -import { Kernel } from '../index'; -import { packageApp } from './lib/package'; -import { getPackageVersion, isPnpmInstalled, isUvInstalled, zipDirectory } from './lib/util'; - -const program = new Command(); - -// When we package a ts app, we have the option to use a custom kernel sdk dependency in package.json. -// This is useful for local dev. -// KERNEL_NODE_SDK_OVERRIDE=/Users/rafaelgarcia/code/onkernel/kernel/packages/sdk-node -// KERNEL_NODE_SDK_OVERRIDE_VERSION=0.0.1alpha.1 -const KERNEL_NODE_SDK_OVERRIDE = process.env['KERNEL_NODE_SDK_OVERRIDE'] || undefined; -// Same for python... -// KERNEL_PYTHON_SDK_OVERRIDE=/Users/rafaelgarcia/code/onkernel/kernel/packages/sdk-python -// KERNEL_PYTHON_SDK_OVERRIDE_VERSION=0.0.1alpha.1 -const KERNEL_PYTHON_SDK_OVERRIDE = process.env['KERNEL_PYTHON_SDK_OVERRIDE'] || undefined; - -// Point to a local version of the boot loader or a specific version -const KERNEL_NODE_BOOT_LOADER_OVERRIDE = process.env['KERNEL_NODE_BOOT_LOADER_OVERRIDE'] || undefined; -const KERNEL_PYTHON_BOOT_LOADER_OVERRIDE = process.env['KERNEL_PYTHON_BOOT_LOADER_OVERRIDE'] || undefined; - -if (process.argv.length === 3 && ['-v', '--version'].includes(process.argv[2]!)) { - console.log(getPackageVersion()); - process.exit(0); -} - -program.name('kernel').description('CLI for Kernel deployment and invocation'); - -program - .command('deploy') - .description('Deploy a Kernel application') - .argument('', 'Path to entrypoint file (TypeScript or Python)') - .option('--local', 'Does not publish the app to Kernel, but installs it on disk for invoking locally') - .option('--version ', 'Specify a version for the app (defaults to current timestamp)') - .action(async (entrypoint, options) => { - const resolvedEntrypoint = path.resolve(entrypoint); - if (!fs.existsSync(resolvedEntrypoint)) { - console.error(`Error: Entrypoint ${resolvedEntrypoint} doesn't exist`); - process.exit(1); - } - - // package up the app for either uploading or local deployment - const dotKernelDir = await packageApp({ - sourceDir: path.dirname(resolvedEntrypoint), // TODO: handle nested entrypoint, i.e. ./src/entrypoint.ts - entrypoint: resolvedEntrypoint, - sdkOverrides: { - ...(KERNEL_NODE_SDK_OVERRIDE && { node: KERNEL_NODE_SDK_OVERRIDE }), - ...(KERNEL_PYTHON_SDK_OVERRIDE && { python: KERNEL_PYTHON_SDK_OVERRIDE }), - }, - bootLoaderOverrides: { - ...(KERNEL_NODE_BOOT_LOADER_OVERRIDE && { node: KERNEL_NODE_BOOT_LOADER_OVERRIDE }), - ...(KERNEL_PYTHON_BOOT_LOADER_OVERRIDE && { python: KERNEL_PYTHON_BOOT_LOADER_OVERRIDE }), - }, - }); - - if (options.local) { - const kernelJson = JSON.parse( - fs.readFileSync(path.join(dotKernelDir, 'app', 'kernel.json'), 'utf8'), - ) as KernelJson; - for (const app of kernelJson.apps) { - if (!app.actions || app.actions.length === 0) { - console.error(`App "${app.name}" has no actions`); - process.exit(1); - } - console.log( - `App "${app.name}" successfully deployed locally and ready to \`kernel invoke --local ${quoteIfNeeded(app.name)} ${quoteIfNeeded(app.actions[0]!.name)}\``, - ); - } - } else { - if (!process.env['KERNEL_API_KEY']) { - console.error('Error: KERNEL_API_KEY environment variable is not set'); - console.error('Please set your Kernel API key using: export KERNEL_API_KEY=your_api_key'); - process.exit(1); - } - - // Read kernel.json to get app info - const kernelJson = JSON.parse( - fs.readFileSync(path.join(dotKernelDir, 'app', 'kernel.json'), 'utf8'), - ) as KernelJson; - - if (!kernelJson.apps || kernelJson.apps.length === 0) { - console.error('Error: No apps found in kernel.json'); - process.exit(1); - } - - const appName = kernelJson.apps[0]?.name; - if (!appName) { - console.error('Error: App name not found in kernel.json'); - process.exit(1); - } - - // Create a Kernel client - const client = new Kernel({ - apiKey: process.env['KERNEL_API_KEY'], - baseURL: process.env['KERNEL_BASE_URL'] || 'http://localhost:3001', - }); - - // Set version (use provided version or generate from timestamp) - const version = options.version || Date.now().toString(); - - console.log(chalk.green(`Compressing files...`)); - const tmpZipFile = tmp.fileSync({ postfix: '.zip' }); - - try { - // Zip the packaged app - await zipDirectory(path.join(dotKernelDir, 'app'), tmpZipFile.name); - - console.log(chalk.green(`Uploading app "${appName}" (version: ${version})...`)); - - // Deploy to Kernel - const response = await client.apps.deploy({ - appName: appName, - file: createReadStream(tmpZipFile.name), - version: version, - }); - - console.log(chalk.green(`App "${appName}" successfully deployed to Kernel`)); - console.log( - `You can invoke it with: kernel invoke --version ${version} ${quoteIfNeeded(appName)} ${quoteIfNeeded(kernelJson.apps[0]!.actions[0]!.name)} PAYLOAD`, - ); - } catch (error) { - console.error('Error deploying to Kernel:', error); - process.exit(1); - } finally { - // Clean up temp file - tmpZipFile.removeCallback(); - } - } - }); - -function quoteIfNeeded(str: string) { - if (str.includes(' ')) { - return `"${str}"`; - } - return str; -} - -program - .command('invoke') - .description('Invoke a deployed Kernel application') - .option('--local', 'Invoke a locally deployed application') - .option('--version ', 'Specify a version of the app to invoke') - .argument('', 'Name of the application to invoke') - .argument('', 'Name of the action to invoke') - .argument('', 'JSON payload to send to the application') - .action(async (appName, actionName, payload, options) => { - let parsedPayload; - try { - parsedPayload = JSON.parse(payload); - } catch (error) { - console.error('Error: Invalid JSON payload'); - process.exit(1); - } - - if (!options.local) { - if (!process.env['KERNEL_API_KEY']) { - console.error('Error: KERNEL_API_KEY environment variable is not set'); - console.error('Please set your Kernel API key using: export KERNEL_API_KEY=your_api_key'); - process.exit(1); - } - - // Create a Kernel client - const client = new Kernel({ - apiKey: process.env['KERNEL_API_KEY'], - baseURL: process.env['KERNEL_BASE_URL'] || 'http://localhost:3001', - }); - - console.log(`Invoking "${appName}" with action "${actionName}" and payload:`); - console.log(JSON.stringify(parsedPayload, null, 2)); - - try { - const response = await client.apps.invoke({ - appName, - actionName, - payload, - ...(options.version && { version: options.version }), - }); - - console.log('Result:'); - console.log(JSON.stringify(JSON.parse(response.output || '{}'), null, 2)); - } catch (error) { - console.error('Error invoking application:', error); - process.exit(1); - } - return; - } - - console.log(`Invoking "${appName}" with action "${actionName}" and payload:`); - console.log(JSON.stringify(parsedPayload, null, 2)); - - // Get the app directory - const cacheFile = path.join(os.homedir(), '.local', 'state', 'kernel', 'deploy', 'local', appName); - if (!fs.existsSync(cacheFile)) { - console.error(`Error: App "${appName}" local deployment not found. `); - console.error('Did you `kernel deploy --local `?'); - process.exit(1); - } - const kernelLocalDir = fs.readFileSync(cacheFile, 'utf8').trim(); - if (!fs.existsSync(kernelLocalDir)) { - console.error(`Error: App "${appName}" local deployment has been corrupted, please re-deploy.`); - process.exit(1); - } - - const isPythonApp = fs.existsSync(path.join(kernelLocalDir, 'pyproject.toml')); - const isTypeScriptApp = fs.existsSync(path.join(kernelLocalDir, 'package.json')); - const invokeOptions: InvokeLocalOptions = { - kernelLocalDir, - appName, - actionName, - parsedPayload, - }; - try { - if (isPythonApp) { - await invokeLocalPython(invokeOptions); - } else if (isTypeScriptApp) { - await invokeLocalNode(invokeOptions); - } else { - throw new Error(`Unsupported app type in ${kernelLocalDir}`); - } - } catch (error) { - console.error('Error invoking application:', error); - process.exit(1); - } - }); - -/** - * Waits for a process to output a startup message while echoing stderr - */ -async function waitForStartupMessage( - childProcess: { stderr: ReadableStream }, - timeoutMs: number = 30000, -): Promise { - return new Promise(async (resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Timeout waiting for application startup.')); - }, timeoutMs); - - const reader = childProcess.stderr.getReader(); - const decoder = new TextDecoder(); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const text = decoder.decode(value); - process.stderr.write(text); - - if (text.includes('Application startup complete.') || text.includes('Kernel application running')) { - clearTimeout(timeout); - resolve(); - break; - } - } - } finally { - reader.releaseLock(); - } - }); -} - -type InvokeLocalOptions = { - kernelLocalDir: string; - appName: string; - actionName: string; - parsedPayload: any; -}; - -/** - * Invokes a locally deployed Python app action - */ -async function invokeLocalPython({ kernelLocalDir, appName, actionName, parsedPayload }: InvokeLocalOptions) { - const uvInstalled = await isUvInstalled(); - if (!uvInstalled) { - console.error('Error: uv is not installed. Please install it with:'); - console.error(' curl -LsSf https://astral.sh/uv/install.sh | sh'); - process.exit(1); - } - - // load kernel.json for entrypoint - const kernelJson = JSON.parse( - fs.readFileSync(path.join(kernelLocalDir, 'app', 'kernel.json'), 'utf8'), - ) as KernelJson; - const entrypoint = kernelJson.entrypoint; - if (!entrypoint) { - throw new Error('Local deployment does not have an entrypoint, please try re-deploying.'); - } - - // Find an available port and start the boot loader - const port = await getPort(); - const pythonProcess = Bun.spawn( - ['uv', 'run', '--no-cache', 'python', 'main.py', './app', '--port', port.toString()], - { - cwd: kernelLocalDir, - stdio: ['inherit', 'inherit', 'pipe'], - env: process.env, - }, - ); - try { - await waitForStartupMessage(pythonProcess); - } catch (error) { - console.error('Error while waiting for application to start:', error); - pythonProcess.kill(); - process.exit(1); - } - - try { - await requestAppAction({ port, appName, actionName, parsedPayload }); - } catch (error) { - console.error('Error invoking application:', error); - } finally { - console.log('Shutting down boot server...'); - pythonProcess.kill(); - } -} - -/** - * Invokes a locally deployed TypeScript app action - */ -async function invokeLocalNode({ kernelLocalDir, appName, actionName, parsedPayload }: InvokeLocalOptions) { - const pnpmInstalled = await isPnpmInstalled(); - if (!pnpmInstalled) { - console.error('Error: pnpm is not installed. Please install it with:'); - console.error(' npm install -g pnpm'); - process.exit(1); - } - - // load kernel.json for entrypoint - const kernelJson = JSON.parse( - fs.readFileSync(path.join(kernelLocalDir, 'app', 'kernel.json'), 'utf8'), - ) as KernelJson; - const entrypoint = kernelJson.entrypoint; - if (!entrypoint) { - throw new Error('Local deployment does not have an entrypoint, please try re-deploying.'); - } - - // Find an available port and start the boot loader - const port = await getPort(); - const tsProcess = Bun.spawn( - ['pnpm', 'exec', 'tsx', 'index.ts', '--port', port.toString(), path.join(kernelLocalDir, 'app')], - { - cwd: kernelLocalDir, - stdio: ['inherit', 'inherit', 'pipe'], - env: process.env, - }, - ); - - try { - await waitForStartupMessage(tsProcess); - } catch (error) { - console.error('Error while waiting for application to start:', error); - tsProcess.kill(); - process.exit(1); - } - - try { - await requestAppAction({ port, appName, actionName, parsedPayload }); - } catch (error) { - console.error('Error invoking application:', error); - } finally { - console.log('Shutting down boot server...'); - tsProcess.kill(); - } -} - -async function requestAppAction({ - port, - appName, - actionName, - parsedPayload, -}: { - port: number; - appName: string; - actionName: string; - parsedPayload: any; -}): Promise { - let serverReached = false; - try { - const healthCheck = await fetch(`http://localhost:${port}/`, { - method: 'GET', - }).catch(() => null); - if (!healthCheck) { - throw new Error(`Could not connect to boot server at http://localhost:${port}/`); - } - serverReached = true; - } catch (error) { - console.error('Error connecting to boot server:', error); - console.error('The boot server might not have started correctly.'); - process.exit(1); - } - - const response = await fetch(`http://localhost:${port}/apps/${appName}/actions/${actionName}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(parsedPayload), - }).catch((error) => { - console.error(`Failed to connect to action endpoint: ${error.message}`); - throw new Error( - `Could not connect to action endpoint at http://localhost:${port}/apps/${appName}/actions/${actionName}`, - ); - }); - - if (!response.ok) { - const errorText = await response.text().catch(() => 'Unknown error'); - throw new Error(`HTTP error ${response.status}: ${errorText}`); - } - - const result = await response.json(); - console.log('Result:', JSON.stringify(result, null, 2)); - - return result; -} - -program.parse(); diff --git a/src/cli/lib/constants.ts b/src/cli/lib/constants.ts deleted file mode 100644 index afd892b..0000000 --- a/src/cli/lib/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Constants for package names (subject to change) -export const PYTHON_PACKAGE_NAME = 'kernel'; -export const NODE_PACKAGE_NAME = '@onkernel/sdk'; diff --git a/src/cli/lib/package.ts b/src/cli/lib/package.ts deleted file mode 100644 index 5413e14..0000000 --- a/src/cli/lib/package.ts +++ /dev/null @@ -1,259 +0,0 @@ -import fs from 'fs'; -import fsExtra from 'fs-extra'; -import os from 'os'; -import path from 'path'; -import { parse as parseToml, stringify as stringifyToml } from 'smol-toml'; -import type { KernelJson } from '../../core/app-framework'; -import { NODE_PACKAGE_NAME, PYTHON_PACKAGE_NAME } from './constants'; -import { runInDirectory } from './util'; - -interface PackageConfig { - sourceDir: string; - entrypoint: string; - sdkOverrides: { - node?: string; - python?: string; - }; - bootLoaderOverrides: { - node?: string; - python?: string; - }; -} - -/** - * Package a Kernel application. - * We use a boot loader to export the kernel.json. - * - * @param config - The configuration for the package operation - * @returns Path to the folder containing the kernel.json file + boot loader ready to run the app locally or upload to the cloud. - */ -export async function packageApp(config: PackageConfig): Promise { - const { sourceDir, entrypoint, sdkOverrides, bootLoaderOverrides } = config; - const extension = path.extname(entrypoint); - - // Define our directory structure - const kernelDir = path.resolve(path.join(sourceDir, '.kernel')); - const kernelLocalDir = path.join(kernelDir, 'local'); - const kernelAppDir = path.join(kernelLocalDir, 'app'); - - // Clean and create directories - if (fs.existsSync(kernelLocalDir)) { - fs.rmSync(kernelLocalDir, { recursive: true, force: true }); - } - fs.mkdirSync(kernelLocalDir, { recursive: true }); - fs.mkdirSync(kernelAppDir, { recursive: true }); - - // Copy user code dir to a temporary directory first, then move to .kernel/local/app directory - // This is to avoid the error of copying a directory to a subdirectory of itself - const tmpDir = path.join(os.tmpdir(), `kernel-${Date.now()}`); - fs.mkdirSync(tmpDir, { recursive: true }); - copyDirectoryContents(sourceDir, tmpDir, ['.kernel']); - if (fs.existsSync(kernelAppDir)) { - fs.rmSync(kernelAppDir, { recursive: true, force: true }); - } - fs.renameSync(tmpDir, kernelAppDir); - - // The generated kernel.json path - const kernelJsonPath = path.join(kernelAppDir, 'kernel.json'); - - // Determine the boot loader directory based on file extension - let bootLoaderPath; - if (extension === '.py' && bootLoaderOverrides.python) { - bootLoaderPath = bootLoaderOverrides.python; - } else if (extension === '.ts' && bootLoaderOverrides.node) { - bootLoaderPath = bootLoaderOverrides.node; - } else { - // eventually this will be a default boot loader downloaded from somewhere - throw new Error(`No boot loader specified for ${extension}`); - } - - // Copy the boot loader to .kernel/local - copyDirectoryContents(bootLoaderPath, kernelLocalDir); - - if (extension === '.py') { - // 1. Update kernel SDK dependency if override is provided - if (sdkOverrides.python) { - if (fs.existsSync(path.join(kernelAppDir, 'pyproject.toml'))) { - overwriteKernelDependencyInPyproject(path.join(kernelAppDir, 'pyproject.toml'), sdkOverrides.python); - } - if (fs.existsSync(path.join(kernelAppDir, 'requirements.txt'))) { - overwriteKernelDependencyInRequirementsTxt( - path.join(kernelAppDir, 'requirements.txt'), - sdkOverrides.python, - ); - } - if (fs.existsSync(path.join(kernelLocalDir, 'pyproject.toml'))) { - overwriteKernelDependencyInPyproject( - path.join(kernelLocalDir, 'pyproject.toml'), - sdkOverrides.python, - ); - } - } - - // 2. Generate app_requirements.txt for merging with boot loader's pyproject.toml via uv add -r ./app/app_requirements.txt - if (fs.existsSync(path.join(kernelAppDir, 'requirements.txt'))) { - // If requirements.txt exists, just copy it to app_requirements.txt - fs.copyFileSync( - path.join(kernelAppDir, 'requirements.txt'), - path.join(kernelAppDir, 'app_requirements.txt'), - ); - } else { - // Otherwise use uv to generate requirements - await runInDirectory( - `/bin/bash -c 'uv venv && \ - source .venv/bin/activate && \ - uv sync && \ - uv pip freeze > app_requirements.txt && \ - rm -rf .venv && \ - rm -rf __pycache__'`, - kernelAppDir, - ); - } - - // 3. Install app requirements in boot loader environment - await runInDirectory( - `/bin/bash -c 'uv venv && \ - source .venv/bin/activate && \ - uv add -r ./app/app_requirements.txt'`, - kernelLocalDir, - ); - - // 4. Run the boot loader with --export - await runInDirectory( - `/bin/bash -c 'source .venv/bin/activate && \ - uv run python main.py --export ${kernelJsonPath} --entrypoint-relpath ${path.relative(sourceDir, entrypoint)} ${path.resolve(kernelAppDir)}'`, - kernelLocalDir, - ); - } else if (extension === '.ts') { - // 1. Merge package.json dependencies - const appPackageJsonPath = path.join(kernelAppDir, 'package.json'); - const bootPackageJsonPath = path.join(kernelLocalDir, 'package.json'); - if (!fs.existsSync(appPackageJsonPath)) { - throw new Error('No package.json found in user code'); - } - const appPackageJson = JSON.parse(fs.readFileSync(appPackageJsonPath, 'utf8')); - const bootPackageJson = JSON.parse(fs.readFileSync(bootPackageJsonPath, 'utf8')); - if (appPackageJson.dependencies) { - if (!bootPackageJson.dependencies) { - bootPackageJson.dependencies = {}; - } - for (const [depName, depVersion] of Object.entries(appPackageJson.dependencies)) { - if (!bootPackageJson.dependencies[depName]) { - bootPackageJson.dependencies[depName] = depVersion; - } - } - } - if (appPackageJson.devDependencies) { - if (!bootPackageJson.devDependencies) { - bootPackageJson.devDependencies = {}; - } - for (const [depName, depVersion] of Object.entries(appPackageJson.devDependencies)) { - if (!bootPackageJson.devDependencies[depName]) { - bootPackageJson.devDependencies[depName] = depVersion; - } - } - } - - // Override kernel SDK if specified - if (sdkOverrides.node) { - bootPackageJson.dependencies[NODE_PACKAGE_NAME] = sdkOverrides.node; - } - fs.writeFileSync(bootPackageJsonPath, JSON.stringify(bootPackageJson, null, 2)); - - // 2. Install dependencies - await runInDirectory('pnpm i', kernelLocalDir); - - // 3. Run the boot loader with --export - await runInDirectory( - `pnpm exec tsx index.ts --export ${kernelJsonPath} \ - --entrypoint-relpath ${path.relative(sourceDir, entrypoint)} \ - ${path.resolve(kernelAppDir)}`, - kernelLocalDir, - ); - } else { - throw new Error(`Unsupported file extension: ${extension}`); - } - - // Verify kernel.json was created - if (!fs.existsSync(kernelJsonPath)) { - throw new Error('Failed to create kernel.json'); - } - - // cache a mapping from app name to the kernalLocalDir - // this lets us know where to invoke the app locally when the user runs `kernel invoke --local ' - const cacheDir = path.join(os.homedir(), '.local', 'state', 'kernel', 'deploy', 'local'); - fs.mkdirSync(cacheDir, { recursive: true }); - const kernelJson = JSON.parse(fs.readFileSync(kernelJsonPath, 'utf8')) as KernelJson; - for (const app of kernelJson.apps) { - fs.writeFileSync(path.join(cacheDir, app.name), kernelLocalDir); - } - return kernelLocalDir; -} - -/** - * Copy all files from source directory to target directory - */ -export function copyDirectoryContents(sourceDir: string, targetDir: string, excludeDirs: string[] = []) { - fsExtra.copySync(sourceDir, targetDir, { - filter: (src: string) => { - const basename = path.basename(src); - const standardExcludes = ['.build', 'node_modules', '.git', '.mypy_cache', '.venv', '__pycache__']; - return ![...standardExcludes, ...excludeDirs].includes(basename); - }, - overwrite: true, - }); -} - -function overwriteKernelDependencyInPyproject(pyprojectPath: string, kernelDependencyOverride: string) { - const pyproject = parseToml(fs.readFileSync(pyprojectPath, 'utf8')) as any; - if (!pyproject.project) { - pyproject.project = { dependencies: [] }; - } else if (!pyproject.project.dependencies) { - pyproject.project.dependencies = []; - } - pyproject.project.dependencies = pyproject.project.dependencies.filter((dep: string) => { - return !dep.startsWith(PYTHON_PACKAGE_NAME); - }); - // If it's a path, add it to tool.uv.sources instead - if ( - !kernelDependencyOverride.startsWith('/') && - !kernelDependencyOverride.startsWith('./') && - !kernelDependencyOverride.startsWith('../') - ) { - pyproject.project.dependencies.push(kernelDependencyOverride); - } else { - pyproject.project.dependencies.push(PYTHON_PACKAGE_NAME); - if (!pyproject.tool) { - pyproject.tool = {}; - } - if (!pyproject.tool.uv) { - pyproject.tool.uv = {}; - } - if (!pyproject.tool.uv.sources) { - pyproject.tool.uv.sources = {}; - } - pyproject.tool.uv.sources.kernel = { path: kernelDependencyOverride }; - } - fs.writeFileSync(pyprojectPath, stringifyToml(pyproject)); -} - -function overwriteKernelDependencyInRequirementsTxt( - requirementsTxtPath: string, - kernelDependencyOverride: string, -) { - const requirementsTxt = fs.readFileSync(requirementsTxtPath, 'utf8'); - const requirementsTxtLines = requirementsTxt.split('\n'); - const newRequirementsTxtLines = requirementsTxtLines.filter((line: string) => { - return !line.startsWith(PYTHON_PACKAGE_NAME); - }); - if ( - !kernelDependencyOverride.startsWith('/') && - !kernelDependencyOverride.startsWith('./') && - !kernelDependencyOverride.startsWith('../') - ) { - newRequirementsTxtLines.push(`${PYTHON_PACKAGE_NAME} @ file:${kernelDependencyOverride}`); - } else { - newRequirementsTxtLines.push(kernelDependencyOverride); - } - fs.writeFileSync(requirementsTxtPath, newRequirementsTxtLines.join('\n')); -} diff --git a/src/cli/lib/util.ts b/src/cli/lib/util.ts deleted file mode 100644 index d7db18c..0000000 --- a/src/cli/lib/util.ts +++ /dev/null @@ -1,122 +0,0 @@ -import archiver from 'archiver'; -import { execa } from 'execa'; -import fs from 'fs'; -import fsExtra from 'fs-extra'; -import walk from 'ignore-walk'; -import path from 'path'; -import type { PackageJson } from 'type-fest'; - -/** - * Run a command and return only the exit code - * - * @param command The command to run - * @param options Optional options including working directory - * @returns Promise resolving to just the exit code - */ -export async function runForExitCode(command: string, options: { cwd?: string } = {}): Promise { - try { - const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd(); - - // Run with stdio: 'ignore' to suppress output - const { exitCode } = await execa(command, { - shell: true, - cwd, - stdio: 'ignore', // Don't show any output - reject: false, // Don't throw on non-zero exit code - }); - - return exitCode ?? 1; // Handle undefined by returning 1 - } catch (error) { - // In case of other errors (not command execution errors) - return 1; - } -} - -/** - * Run a command in a specific directory that inherits IO and throws on error - * - * @param command The command to run - * @param cwd Working directory - * @returns Promise that resolves when command completes successfully or rejects on error - */ -export async function runInDirectory(command: string, cwd: string): Promise { - const resolvedCwd = path.resolve(cwd); - - // Run with stdio: 'inherit' to show output - await execa(command, { - shell: true, - cwd: resolvedCwd, - stdio: 'inherit', - }); -} - -/** - * Get the version from the package.json file. - * - * @returns Promise resolving to the package version string - */ -export function getPackageVersion(): string { - const pkgJsonPath = path.join(__dirname, '..', '..', '..', 'package.json'); - const content = fsExtra.readJSONSync(pkgJsonPath) as PackageJson; - if (!content.version) { - throw new Error('package.json does not contain a version'); - } - return content.version; -} - -/** - * Checks if uv is installed - */ -export async function isUvInstalled(): Promise { - const exitCode = await runForExitCode('uv --version'); - return exitCode === 0; -} - -/** - * Checks if pnpm is installed. - */ -export async function isPnpmInstalled(): Promise { - const exitCode = await runForExitCode('pnpm --version'); - return exitCode === 0; -} - -/** - * Zips a directory into a file - * - * @param sourceDir Directory to zip - * @param outPath Path to output zip file - * @returns Promise that resolves when zip is complete - */ -export async function zipDirectory(inputDir: string, outputZip: string): Promise { - const entries = await walk({ - path: inputDir, - ignoreFiles: ['.gitignore', '.dockerignore'], - includeEmpty: true, - follow: false, - }); - - const output = fs.createWriteStream(outputZip); - const archive = archiver('zip', { zlib: { level: 9 } }); - - const finalizePromise = new Promise((resolve, reject) => { - output.on('close', resolve); - archive.on('error', reject); - }); - - archive.pipe(output); - - for (const entry of entries) { - const fullPath = path.join(inputDir, entry); - const stat = fs.statSync(fullPath); - const archivePath = entry.split(path.sep).join('/'); // Normalize to Unix slashes - - if (stat.isFile()) { - archive.file(fullPath, { name: archivePath }); - } else if (stat.isDirectory()) { - archive.append('', { name: archivePath.endsWith('/') ? archivePath : archivePath + '/' }); - } - } - - await archive.finalize(); - await finalizePromise; -}