Initial open-source release
This commit is contained in:
53
src/ipc/utils/file_utils.ts
Normal file
53
src/ipc/utils/file_utils.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { promises as fsPromises } from "node:fs";
|
||||
|
||||
/**
|
||||
* Recursively gets all files in a directory, excluding node_modules and .git
|
||||
* @param dir The directory to scan
|
||||
* @param baseDir The base directory for calculating relative paths
|
||||
* @returns Array of file paths relative to the base directory
|
||||
*/
|
||||
export function getFilesRecursively(dir: string, baseDir: string): string[] {
|
||||
if (!fs.existsSync(dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dirents = fs.readdirSync(dir, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
|
||||
for (const dirent of dirents) {
|
||||
const res = path.join(dir, dirent.name);
|
||||
if (dirent.isDirectory()) {
|
||||
// For directories, concat the results of recursive call
|
||||
// Exclude node_modules and .git directories
|
||||
if (dirent.name !== "node_modules" && dirent.name !== ".git") {
|
||||
files.push(...getFilesRecursively(res, baseDir));
|
||||
}
|
||||
} else {
|
||||
// For files, add the relative path
|
||||
files.push(path.relative(baseDir, res));
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export async function copyDirectoryRecursive(
|
||||
source: string,
|
||||
destination: string
|
||||
) {
|
||||
await fsPromises.mkdir(destination, { recursive: true });
|
||||
const entries = await fsPromises.readdir(source, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(source, entry.name);
|
||||
const destPath = path.join(destination, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyDirectoryRecursive(srcPath, destPath);
|
||||
} else {
|
||||
await fsPromises.copyFile(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/ipc/utils/get_model_client.ts
Normal file
65
src/ipc/utils/get_model_client.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { createOpenAI } from "@ai-sdk/openai";
|
||||
import { createGoogleGenerativeAI as createGoogle } from "@ai-sdk/google";
|
||||
import { createAnthropic } from "@ai-sdk/anthropic";
|
||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
||||
import type { LargeLanguageModel, UserSettings } from "../../lib/schemas";
|
||||
import { PROVIDER_TO_ENV_VAR, AUTO_MODELS } from "../../constants/models";
|
||||
|
||||
export function getModelClient(
|
||||
model: LargeLanguageModel,
|
||||
settings: UserSettings
|
||||
) {
|
||||
// Handle 'auto' provider by trying each model in AUTO_MODELS until one works
|
||||
if (model.provider === "auto") {
|
||||
// Try each model in AUTO_MODELS in order until finding one with an API key
|
||||
for (const autoModel of AUTO_MODELS) {
|
||||
const apiKey =
|
||||
settings.providerSettings?.[autoModel.provider]?.apiKey ||
|
||||
process.env[PROVIDER_TO_ENV_VAR[autoModel.provider]];
|
||||
|
||||
if (apiKey) {
|
||||
console.log(
|
||||
`Using provider: ${autoModel.provider} model: ${autoModel.name}`
|
||||
);
|
||||
// Use the first model that has an API key
|
||||
return getModelClient(
|
||||
{
|
||||
provider: autoModel.provider,
|
||||
name: autoModel.name,
|
||||
} as LargeLanguageModel,
|
||||
settings
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If no models have API keys, throw an error
|
||||
throw new Error("No API keys available for any model in AUTO_MODELS");
|
||||
}
|
||||
|
||||
const apiKey =
|
||||
settings.providerSettings?.[model.provider]?.apiKey ||
|
||||
process.env[PROVIDER_TO_ENV_VAR[model.provider]];
|
||||
switch (model.provider) {
|
||||
case "openai": {
|
||||
const provider = createOpenAI({ apiKey });
|
||||
return provider(model.name);
|
||||
}
|
||||
case "anthropic": {
|
||||
const provider = createAnthropic({ apiKey });
|
||||
return provider(model.name);
|
||||
}
|
||||
case "google": {
|
||||
const provider = createGoogle({ apiKey });
|
||||
return provider(model.name);
|
||||
}
|
||||
case "openrouter": {
|
||||
const provider = createOpenRouter({ apiKey });
|
||||
return provider(model.name);
|
||||
}
|
||||
default: {
|
||||
// Ensure exhaustive check if more providers are added
|
||||
const _exhaustiveCheck: never = model.provider;
|
||||
throw new Error(`Unsupported model provider: ${model.provider}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/ipc/utils/lock_utils.ts
Normal file
51
src/ipc/utils/lock_utils.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Track app operations that are in progress
|
||||
const appOperationLocks = new Map<number, Promise<void>>();
|
||||
|
||||
/**
|
||||
* Acquires a lock for an app operation
|
||||
* @param appId The app ID to lock
|
||||
* @returns An object with release function and promise
|
||||
*/
|
||||
export function acquireLock(appId: number): {
|
||||
release: () => void;
|
||||
promise: Promise<void>;
|
||||
} {
|
||||
let release: () => void = () => {};
|
||||
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
release = () => {
|
||||
appOperationLocks.delete(appId);
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
|
||||
appOperationLocks.set(appId, promise);
|
||||
return { release, promise };
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a function with a lock on the app ID
|
||||
* @param appId The app ID to lock
|
||||
* @param fn The function to execute with the lock
|
||||
* @returns Result of the function
|
||||
*/
|
||||
export async function withLock<T>(
|
||||
appId: number,
|
||||
fn: () => Promise<T>
|
||||
): Promise<T> {
|
||||
// Wait for any existing operation to complete
|
||||
const existingLock = appOperationLocks.get(appId);
|
||||
if (existingLock) {
|
||||
await existingLock;
|
||||
}
|
||||
|
||||
// Acquire a new lock
|
||||
const { release, promise } = acquireLock(appId);
|
||||
|
||||
try {
|
||||
const result = await fn();
|
||||
return result;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
104
src/ipc/utils/process_manager.ts
Normal file
104
src/ipc/utils/process_manager.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { ChildProcess } from "node:child_process";
|
||||
import treeKill from "tree-kill";
|
||||
|
||||
// Define a type for the value stored in runningApps
|
||||
export interface RunningAppInfo {
|
||||
process: ChildProcess;
|
||||
processId: number;
|
||||
}
|
||||
|
||||
// Store running app processes
|
||||
export const runningApps = new Map<number, RunningAppInfo>();
|
||||
// Global counter for process IDs
|
||||
let processCounterValue = 0;
|
||||
|
||||
// Getter and setter for processCounter to allow modification from outside
|
||||
export const processCounter = {
|
||||
get value(): number {
|
||||
return processCounterValue;
|
||||
},
|
||||
set value(newValue: number) {
|
||||
processCounterValue = newValue;
|
||||
},
|
||||
increment(): number {
|
||||
return ++processCounterValue;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Kills a running process with its child processes
|
||||
* @param process The child process to kill
|
||||
* @param pid The process ID
|
||||
* @returns A promise that resolves when the process is closed or timeout
|
||||
*/
|
||||
export function killProcess(process: ChildProcess): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
// Add timeout to prevent hanging
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn(
|
||||
`Timeout waiting for process (PID: ${process.pid}) to close. Force killing may be needed.`
|
||||
);
|
||||
resolve();
|
||||
}, 5000); // 5-second timeout
|
||||
|
||||
process.on("close", (code, signal) => {
|
||||
clearTimeout(timeout);
|
||||
console.log(
|
||||
`Received 'close' event for process (PID: ${process.pid}) with code ${code}, signal ${signal}.`
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Handle potential errors during kill/close sequence
|
||||
process.on("error", (err) => {
|
||||
clearTimeout(timeout);
|
||||
console.error(
|
||||
`Error during stop sequence for process (PID: ${process.pid}): ${err.message}`
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Ensure PID exists before attempting to kill
|
||||
if (process.pid) {
|
||||
// Use tree-kill to terminate the entire process tree
|
||||
console.log(
|
||||
`Attempting to tree-kill process tree starting at PID ${process.pid}.`
|
||||
);
|
||||
treeKill(process.pid, "SIGTERM", (err: Error | undefined) => {
|
||||
if (err) {
|
||||
console.warn(
|
||||
`tree-kill error for PID ${process.pid}: ${err.message}`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`tree-kill signal sent successfully to PID ${process.pid}.`
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn(`Cannot tree-kill process: PID is undefined.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an app from the running apps map if it's the current process
|
||||
* @param appId The app ID
|
||||
* @param process The process to check against
|
||||
*/
|
||||
export function removeAppIfCurrentProcess(
|
||||
appId: number,
|
||||
process: ChildProcess
|
||||
): void {
|
||||
const currentAppInfo = runningApps.get(appId);
|
||||
if (currentAppInfo && currentAppInfo.process === process) {
|
||||
runningApps.delete(appId);
|
||||
console.log(
|
||||
`Removed app ${appId} (processId ${currentAppInfo.processId}) from running map. Current size: ${runningApps.size}`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`App ${appId} process was already removed or replaced in running map. Ignoring.`
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user