diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..dd8ca7a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(grep:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index af3b460..0000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -tailwind.config.js -vite.config.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 6c95b94..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extends: ['plugin:prettier/recommended'], - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module' - } -}; diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fc8b2e7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,112 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Larabase is an Electron desktop application specifically designed for Laravel developers. It's an opinionated database GUI tool with seamless Laravel integration, providing specialized features for Laravel project development workflows. + +## Key Commands + +### Development + +```bash +# Start development server +npm run dev + +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:mac-apple # macOS (Apple Silicon) +npm run build:mac-intel # macOS (Intel) +npm run build:win # Windows +npm run build:linux # Linux + +# Code Quality +npm run lint # Run ESLint with auto-fix +npm run format # Run Prettier formatter +``` + +## Architecture Overview + +Larabase follows the standard Electron architecture with main and renderer processes: + +### Main Process (`electron/`) + +- **Main Entry:** `electron/main/index.ts` - Application initialization, window management +- **Modules:** `electron/modules/` - Backend functionality implemented as service modules + - Database operations (`mysql.ts`, `tables.ts`, `sql-executor.ts`) + - SSH tunneling (`ssh.ts`) + - Redis integration (`redis.ts`) + - Project management (`projects.ts`) + - Terminal emulation (`terminal.ts`) +- **Helpers:** `electron/helpers/` - Utility functions for various operations + +### Renderer Process (`src/`) + +- **Components:** `src/components/` - Vue.js UI components organized by functionality +- **State Management:** `src/store/` - Pinia stores for application state +- **Services:** `src/services/` - API communication and frontend services +- **Types:** `src/types/` - TypeScript interfaces and type definitions + +### Communication Pattern + +The application follows a communication pattern using Electron's IPC: + +1. Front-end requests through `window.electron.ipcRenderer.invoke(channel, ...args)` +2. Backend handles via IPC handlers registered in modules +3. Results returned to front-end asynchronously + +## Key Features & Implementation + +### Database Connections + +- Supports direct MySQL connections and SSH tunneling +- Connection details stored securely via electron-store +- SSH tunneling implemented using ssh2 library and dynamic port forwarding + +### Query Management + +- Monaco editor for SQL editing with syntax highlighting +- SQL execution handled through mysql2 library +- Results displayed in Vue data table components with sorting/filtering + +### Laravel Integration + +- Specialized views for Laravel-specific database operations +- Migration management interface +- .env file editor +- Terminal integration for running Artisan commands + +### Redis Support + +- Redis database browsing and management +- Key-value storage interaction +- Cache management operations + +## Data Flow + +1. User interacts with Vue UI components +2. Actions trigger state changes in Pinia stores +3. Stores dispatch IPC calls to Electron backend +4. Backend modules execute operations and return results +5. UI updates based on returned data +6. Always use preload file to expose necessary APIs to the renderer process +7. Do not comment on the code unless extremely necessary to avoid polluting the codebase +8. Always create a module in the electron/modules for grouped functionality (e.g., database, SSH, Redis) to maintain organization + +## Technology Stack + +- **Electron**: Cross-platform desktop framework +- **Vue.js 3**: Frontend framework with TypeScript +- **Pinia**: State management +- **Tailwind CSS & DaisyUI**: Styling +- **Monaco Editor**: Code editor component +- **MySQL2**: Database connectivity +- **SSH2**: SSH tunneling support +- **IoRedis**: Redis client functionality + +## Code Tips and Tricks + +- Always use toRaw for reactive objects to avoid errors like "An object could not be cloned." diff --git a/README.md b/README.md index 50577f5..899baf4 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,48 @@ npm run build:win # Windows npm run build:linux # Linux ``` +## SSH Tunneling Support + +Larabase now supports connecting to MySQL databases through SSH tunnels. This feature allows you to: + +1. Connect to remote database servers securely via SSH +2. Work with databases that are not directly accessible from your machine +3. Maintain the same workflow and functionality as with local database connections + +### How SSH Tunneling Works + +SSH tunneling creates a secure encrypted channel between your local machine and the remote server: + +1. An SSH connection is established to the remote server +2. A local port is forwarded to the remote database server port +3. Your connection uses this local port, which securely tunnels traffic to the remote server +4. All database operations work transparently through this tunnel + +### Implementation Details + +The SSH tunneling implementation uses: + +- The `ssh2` library for SSH connections and port forwarding +- The `portfinder` library to dynamically find available local ports +- The existing MySQL connection module, modified to work through SSH tunnels + +### Setup + +To use SSH tunneling, you need: + +1. SSH access to the remote server (user/password or key-based authentication) +2. The remote server needs access to the MySQL database +3. Create a connection in Larabase with SSH tunneling enabled + +### Usage + +When creating a new connection, select the SSH option and provide: + +1. SSH server details (host, port, user, password/private key) +2. Remote database details (host, port, user, password, database name) + +Once connected, you can use all Larabase features as you would with a local database. + ## License This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/docs/postgresql-implementation.md b/docs/postgresql-implementation.md deleted file mode 100644 index 21e201b..0000000 --- a/docs/postgresql-implementation.md +++ /dev/null @@ -1,37 +0,0 @@ -# PostgreSQL Implementation Guide - -This guide provides a step-by-step process for adding PostgreSQL support to Larabase. Each step includes detailed instructions and code examples. - -## Implementation Overview - -Larabase currently only supports MySQL databases. This implementation will add support for PostgreSQL while maintaining all current MySQL functionality. - -## Implementation Steps - -1. [Create PostgreSQL Types](./postgresql-implementation/01-types.md) -2. [Set Up PostgreSQL Helper Module](./postgresql-implementation/02-helper-module.md) -3. [Create IPC Handlers](./postgresql-implementation/03-ipc-handlers.md) -4. [Update Database Schema Services](./postgresql-implementation/04-schema-services.md) -5. [Update SQL Executor](./postgresql-implementation/05-sql-executor.md) -6. [Update UI Components](./postgresql-implementation/06-ui-components.md) -7. [Install Dependencies](./postgresql-implementation/07-dependencies.md) -8. [Testing & Verification](./postgresql-implementation/08-testing.md) - -## Implementation Tracking - -Use this section to track your progress through the implementation steps: - -- [ ] Create PostgreSQL Types -- [ ] Set Up PostgreSQL Helper Module -- [ ] Create IPC Handlers -- [ ] Update Database Schema Services -- [ ] Update SQL Executor -- [ ] Update UI Components -- [ ] Install Dependencies -- [ ] Testing & Verification - -## Notes - -- Each markdown file contains detailed instructions and code snippets for the specific implementation step. -- Code samples include file paths indicating where changes should be made. -- Some steps may require additional configuration depending on your development environment. diff --git a/docs/postgresql-implementation/01-types.md b/docs/postgresql-implementation/01-types.md deleted file mode 100644 index c278739..0000000 --- a/docs/postgresql-implementation/01-types.md +++ /dev/null @@ -1,64 +0,0 @@ -# Step 1: Create PostgreSQL Types - -In this step, we'll create the necessary TypeScript type definitions for PostgreSQL connections. - -## Tasks - -- [ ] Create PostgreSQL connection type definition -- [ ] Update project connection type to support PostgreSQL - -## Implementation Details - -### 1. Create PostgreSQL Connection Type - -Create a new file `src/types/postgresql-connection.d.ts` with the following content: - -```typescript -export interface PostgresqlConnection { - database: string; - host: string; - port: number; - user: string; - password: string; - schema?: string; // PostgreSQL has schema concept - ssl?: boolean; // SSL connection option - connectTimeout?: number; -} -``` - -### 2. Update Project Connection Type - -Modify the `src/types/project.d.ts` file to support PostgreSQL connections: - -```typescript -import { MysqlConnection } from './mysql-connection'; -import { PostgresqlConnection } from './postgresql-connection'; // Add this import -import { RedisConnection } from './redis'; -import { DockerInfo } from './docker-info'; - -export interface ProjectConnection { - id?: string | null; - name: string; - projectPath: string; - type: string; // Now can be 'mysql' or 'postgresql' - icon: string | undefined | null; - db_config: MysqlConnection | PostgresqlConnection; // Update this line - redis_config: RedisConnection; - usingSail: boolean; - status: string; - isValid: boolean; - dockerInfo?: DockerInfo | undefined | null; -} - -// Rest of the file remains unchanged -``` - -## Verification - -- Confirm both files have been created/updated correctly -- Ensure there are no TypeScript errors when building the project -- Verify imports resolve correctly - -## Next Steps - -After completing these tasks, proceed to [Step 2: Set Up PostgreSQL Helper Module](./02-helper-module.md). diff --git a/docs/postgresql-implementation/02-helper-module.md b/docs/postgresql-implementation/02-helper-module.md deleted file mode 100644 index aa749d3..0000000 --- a/docs/postgresql-implementation/02-helper-module.md +++ /dev/null @@ -1,209 +0,0 @@ -# Step 2: Set Up PostgreSQL Helper Module - -In this step, we'll create a helper module for PostgreSQL similar to the existing MySQL helper module. This will handle connection pooling, testing connections, and other database operations. - -## Tasks - -- [ ] Install PostgreSQL client library -- [ ] Create PostgreSQL helper module -- [ ] Implement connection pool management -- [ ] Implement connection testing functions - -## Implementation Details - -### 1. Install PostgreSQL Client Library - -First, install the required PostgreSQL client library: - -```bash -npm install pg -npm install @types/pg --save-dev -``` - -### 2. Create PostgreSQL Helper Module - -Create a new file `electron/helpers/postgresql.ts` with the following content: - -```typescript -import { Pool, PoolClient, ClientConfig } from 'pg'; -import { PostgresqlConnection } from '../../src/types/postgresql-connection'; - -const ERROR_MESSAGES = { - '28P01': 'Authentication failed with the provided credentials', - ECONNREFUSED: 'Connection refused - check host and port', - '3D000': (db: string) => `Database '${db}' does not exist` -}; - -const SELECT_TEST_SQL = 'SELECT 1 AS connection_test'; - -const connectionPools = new Map(); - -function validateParams({ host, port, user, database }: PostgresqlConnection) { - if (!host || !port || !user || !database) { - throw new Error('Missing required connection parameters'); - } -} - -function getPoolKey( - config: PostgresqlConnection, - useConnectionDb: boolean, - targetDatabase: string -): string { - const dbName = useConnectionDb ? config.database : targetDatabase; - return `${config.host}:${config.port}:${config.user}:${dbName}:${config.schema || 'public'}`; -} - -function getConnectionOptions( - config: PostgresqlConnection, - { useConnectionDb = true, targetDatabase = '' } = {} -): ClientConfig { - return { - host: config.host, - port: Number(config.port), - user: config.user, - password: config.password || '', - database: useConnectionDb ? config.database : targetDatabase, - schema: config.schema || 'public', - ssl: config.ssl ? { rejectUnauthorized: false } : undefined, - connectionTimeoutMillis: config.connectTimeout || 10000 - }; -} - -function getConnectionPool( - config: PostgresqlConnection, - { useConnectionDb = true, targetDatabase = '' } = {} -): Pool { - const poolKey = getPoolKey(config, useConnectionDb, targetDatabase); - - if (!connectionPools.has(poolKey)) { - const poolConfig = getConnectionOptions(config, { - useConnectionDb, - targetDatabase - }); - - connectionPools.set(poolKey, new Pool(poolConfig)); - } - - return connectionPools.get(poolKey) as Pool; -} - -async function createConnection( - config: PostgresqlConnection, - { useConnectionDb = true, targetDatabase = '' } = {} -): Promise { - validateParams(config); - - const pool = getConnectionPool(config, { useConnectionDb, targetDatabase }); - - return pool.connect(); -} - -async function releaseConnection(client: PoolClient) { - if (client) { - client.release(); - } -} - -async function safeEndConnection(client: PoolClient) { - try { - if (client) { - client.release(); - } - } catch (error) { - console.error('Error releasing PostgreSQL connection:', error); - } -} - -async function closeAllPools() { - const promises: Promise[] = []; - - for (const pool of connectionPools.values()) { - promises.push(pool.end()); - } - - connectionPools.clear(); - - return Promise.all(promises); -} - -async function ping( - config: PostgresqlConnection, - options: { useConnectionDb?: boolean; targetDatabase?: string } = {} -) { - const client = await createConnection(config, options); - - try { - const res = await client.query(SELECT_TEST_SQL); - - if (!Array.isArray(res.rows) || res.rows.length === 0) { - throw new Error('Connection established but query failed'); - } - } finally { - await releaseConnection(client); - } -} - -async function testConnection(config: PostgresqlConnection) { - try { - await ping(config, { useConnectionDb: true }); - - return { success: true, message: 'Connection successful' }; - } catch (err) { - console.error('Error testing PostgreSQL connection:', err); - - const custom = ERROR_MESSAGES[err.code]; - const message = - typeof custom === 'function' - ? custom(config.database) - : custom || err.message; - - return { success: false, message }; - } -} - -export { - createConnection, - releaseConnection, - safeEndConnection, - testConnection, - ping, - closeAllPools -}; -``` - -### 3. Update Main Module Cleanup - -In `electron/main/index.ts`, update the cleanup function to close PostgreSQL pools too: - -```typescript -// Import PostgreSQL helper -import { closeAllPools as closeMysqlPools } from '../helpers/mysql'; -import { closeAllPools as closePostgresPools } from '../helpers/postgresql'; - -// In app.on('window-all-closed') handler: -app.on('window-all-closed', () => { - win = null; - - cleanup(); - - // Close all database connection pools - Promise.all([closeMysqlPools(), closePostgresPools()]) - .catch((err) => { - console.error('Error closing database pools:', err); - }) - .finally(() => { - if (process.platform === 'darwin') app.quit(); - }); -}); -``` - -## Verification - -- Ensure PostgreSQL client libraries are installed correctly -- Verify the helper module is created with all required functions -- Check that connection pooling works correctly -- Test the connection functions with valid and invalid credentials - -## Next Steps - -After completing these tasks, proceed to [Step 3: Create IPC Handlers](./03-ipc-handlers.md). diff --git a/docs/postgresql-implementation/03-ipc-handlers.md b/docs/postgresql-implementation/03-ipc-handlers.md deleted file mode 100644 index db7632d..0000000 --- a/docs/postgresql-implementation/03-ipc-handlers.md +++ /dev/null @@ -1,195 +0,0 @@ -# Step 3: Create IPC Handlers - -In this step, we'll create IPC handlers for PostgreSQL operations, allowing the renderer process (UI) to communicate with the main process for database operations. - -## Tasks - -- [ ] Create PostgreSQL module for IPC handlers -- [ ] Update main process to register PostgreSQL handlers -- [ ] Update preload script to expose PostgreSQL functions - -## Implementation Details - -### 1. Create PostgreSQL IPC Handlers Module - -Create a new file `electron/modules/postgresql.ts` with the following content: - -```typescript -import { ipcMain } from 'electron'; -import { - testConnection, - createConnection, - safeEndConnection -} from '../helpers/postgresql'; -import { PostgresqlConnection } from '../../src/types/postgresql-connection'; - -function registerPostgresqlHandlers() { - ipcMain.handle( - 'test-postgresql-connection', - async (_, config: PostgresqlConnection) => { - return await testConnection(config); - } - ); - - ipcMain.handle( - 'create-postgresql-database', - async (_, config: PostgresqlConnection, databaseName: string) => { - if ( - !config.host || - !config.port || - !config.user || - !config.database - ) { - return { - success: false, - message: 'Missing connection parameters' - }; - } - - let client: any; - - try { - // Connect to the 'postgres' database to create a new database - const tempConfig = { ...config }; - tempConfig.database = 'postgres'; - client = await createConnection(tempConfig); - - // PostgreSQL requires quoted identifiers for database names with special characters - await client.query(`CREATE DATABASE "${databaseName}"`); - - return { - success: true, - message: `Database ${databaseName} created successfully` - }; - } catch (err) { - console.error('Error creating PostgreSQL database:', err); - let msg = err.message; - if (err.code === '28P01') { - msg = 'Authentication failed with the provided credentials'; - } else if (err.code === 'ECONNREFUSED') { - msg = 'Connection refused - check host and port'; - } else if (err.code === '42P04') { - msg = `Database ${databaseName} already exists`; - } - return { - success: false, - message: msg - }; - } finally { - await safeEndConnection(client); - } - } - ); - - ipcMain.handle( - 'list-postgresql-databases', - async (_, config: PostgresqlConnection) => { - if (!config.host || !config.port || !config.user) { - return { - success: false, - message: 'Missing connection parameters', - databases: [] - }; - } - - let client: any; - - try { - // Connect to the 'postgres' database to list all databases - const tempConfig = { ...config }; - tempConfig.database = 'postgres'; - client = await createConnection(tempConfig); - - const result = await client.query( - "SELECT datname FROM pg_database WHERE datistemplate = false AND datname NOT IN ('postgres', 'template0', 'template1')" - ); - - const databases = result.rows.map((row: any) => row.datname); - - return { success: true, databases }; - } catch (err) { - let msg = err.message; - if (err.code === '28P01') { - msg = 'Authentication failed with the provided credentials'; - } else if (err.code === 'ECONNREFUSED') { - msg = 'Connection refused - check host and port'; - } - return { - success: false, - message: msg, - databases: [] - }; - } finally { - await safeEndConnection(client); - } - } - ); -} - -export { registerPostgresqlHandlers }; -``` - -### 2. Update Main Process to Register PostgreSQL Handlers - -Update `electron/main/index.ts` to register the PostgreSQL handlers: - -```typescript -// Add this import -import { registerPostgresqlHandlers } from '../modules/postgresql'; - -// In the registerHandlers function -function registerHandlers(win: BrowserWindow) { - if (handlersRegistered) return; - - registerDatabaseRestoreHandlers(win); - registerMonitoringHandlers(win); - registerProjectHandlers(win); - registerSettingsHandlers(store); - registerTablesHandlers(); - registerMysqlHandlers(); - registerPostgresqlHandlers(); // Add this line - registerRedisHandlers(); - registerPasswordHandlers(); - registerTerminalHandlers(); - registerMigrationHandlers(); - registerSqlExecutorHandlers(); - registerUpdaterHandlers(win); - - handlersRegistered = true; -} -``` - -### 3. Update Preload Script to Expose PostgreSQL Functions - -Update `electron/preload/index.ts` to expose PostgreSQL functions to the renderer process: - -```typescript -// Add new handlers to the exposed IPC functions -contextBridge.exposeInMainWorld('ipcRenderer', { - // Existing handlers... - - // PostgreSQL handlers - testPostgreSQLConnection: (config: any) => { - return ipcRenderer.invoke('test-postgresql-connection', config); - }, - createPostgreSQLDatabase: (config: any, dbName: string) => { - return ipcRenderer.invoke('create-postgresql-database', config, dbName); - }, - listPostgreSQLDatabases: (config: any) => { - return ipcRenderer.invoke('list-postgresql-databases', config); - } - - // Other handlers... -}); -``` - -## Verification - -- Ensure all IPC handlers are properly registered -- Test the PostgreSQL connection function from the UI -- Verify database listing works correctly -- Check error handling for invalid connection parameters - -## Next Steps - -After completing these tasks, proceed to [Step 4: Update Database Schema Services](./04-schema-services.md). diff --git a/docs/postgresql-implementation/04-schema-services.md b/docs/postgresql-implementation/04-schema-services.md deleted file mode 100644 index 6b39917..0000000 --- a/docs/postgresql-implementation/04-schema-services.md +++ /dev/null @@ -1,316 +0,0 @@ -# Step 4: Update Database Schema Services - -In this step, we'll extend the database schema services to support PostgreSQL. This includes creating functions to retrieve the database schema, tables, columns, and relationships. - -## Tasks - -- [ ] Add PostgreSQL schema retrieval functions to tables module -- [ ] Update database schema service to support PostgreSQL -- [ ] Add IPC handlers for PostgreSQL schema operations - -## Implementation Details - -### 1. Add PostgreSQL Schema Retrieval Functions - -Add the following code to `electron/modules/tables.ts`: - -```typescript -// Import additional required modules -import { - createConnection as createPgConnection, - safeEndConnection as safePgEndConnection -} from '../helpers/postgresql'; -import { PostgresqlConnection } from '../../src/types/postgresql-connection'; - -// Add this function to retrieve PostgreSQL database schema -async function getPostgresqlDatabaseSchemaForAIHandler( - _: any, - config: PostgresqlConnection -) { - let client: any; - - try { - client = await createPgConnection(config); - - // Get all tables in the database and schema - const schema = config.schema || 'public'; - const tablesQuery = ` - SELECT table_name as name - FROM information_schema.tables - WHERE table_schema = $1 - AND table_type = 'BASE TABLE' - `; - - const tablesResult = await client.query(tablesQuery, [schema]); - const tables = tablesResult.rows; - - if (tables.length === 0) { - return { success: true, databaseSchema: { tables: [] } }; - } - - const databaseSchema: { tables: any[] } = { tables: [] }; - - for (const table of tables) { - const tableName = table.name; - - // Get column information - const columnsQuery = ` - SELECT - column_name as name, - data_type as type, - is_nullable = 'YES' as nullable, - column_default as default, - '' as extra, - EXISTS ( - SELECT 1 FROM information_schema.table_constraints tc - JOIN information_schema.key_column_usage kcu - ON tc.constraint_name = kcu.constraint_name - WHERE tc.constraint_type = 'PRIMARY KEY' - AND tc.table_schema = $1 - AND tc.table_name = $2 - AND kcu.column_name = columns.column_name - ) as primary_key, - EXISTS ( - SELECT 1 FROM information_schema.table_constraints tc - JOIN information_schema.key_column_usage kcu - ON tc.constraint_name = kcu.constraint_name - WHERE tc.constraint_type = 'FOREIGN KEY' - AND tc.table_schema = $1 - AND tc.table_name = $2 - AND kcu.column_name = columns.column_name - ) as foreign_key - FROM information_schema.columns - WHERE table_schema = $1 - AND table_name = $2 - ORDER BY ordinal_position - `; - - const columnsResult = await client.query(columnsQuery, [ - schema, - tableName - ]); - const structure = columnsResult.rows; - - // Get foreign key information - const foreignKeysQuery = ` - SELECT - tc.constraint_name as name, - kcu.column_name as column, - ccu.table_name as referenced_table, - ccu.column_name as referenced_column, - 'outgoing' as type - FROM - information_schema.table_constraints tc - JOIN information_schema.key_column_usage kcu - ON tc.constraint_name = kcu.constraint_name - JOIN information_schema.constraint_column_usage ccu - ON ccu.constraint_name = tc.constraint_name - WHERE - tc.constraint_type = 'FOREIGN KEY' - AND tc.table_schema = $1 - AND tc.table_name = $2 - `; - - const incomingFKQuery = ` - SELECT - tc.constraint_name as name, - kcu.column_name as column, - kcu.table_name as referenced_table, - ccu.column_name as referenced_column, - 'incoming' as type - FROM - information_schema.table_constraints tc - JOIN information_schema.key_column_usage kcu - ON tc.constraint_name = kcu.constraint_name - JOIN information_schema.constraint_column_usage ccu - ON ccu.constraint_name = tc.constraint_name - WHERE - tc.constraint_type = 'FOREIGN KEY' - AND tc.table_schema = $1 - AND ccu.table_name = $2 - `; - - const outgoingResult = await client.query(foreignKeysQuery, [ - schema, - tableName - ]); - const incomingResult = await client.query(incomingFKQuery, [ - schema, - tableName - ]); - - const foreignKeys = [ - ...outgoingResult.rows, - ...incomingResult.rows - ]; - - // Get index information - const indexesQuery = ` - SELECT - i.relname as name, - CASE - WHEN ix.indisprimary THEN 'PRIMARY' - WHEN ix.indisunique THEN 'UNIQUE' - ELSE 'INDEX' - END as type, - array_to_string(array_agg(a.attname), ',') as columns - FROM - pg_class t, - pg_class i, - pg_index ix, - pg_attribute a, - pg_namespace n - WHERE - t.oid = ix.indrelid - AND i.oid = ix.indexrelid - AND a.attrelid = t.oid - AND a.attnum = ANY(ix.indkey) - AND t.relkind = 'r' - AND t.relname = $2 - AND n.nspname = $1 - AND t.relnamespace = n.oid - GROUP BY - i.relname, ix.indisprimary, ix.indisunique - `; - - const indexesResult = await client.query(indexesQuery, [ - schema, - tableName - ]); - const indexes = indexesResult.rows.map((idx: any) => { - return { - ...idx, - columns: idx.columns.split(',') - }; - }); - - databaseSchema.tables.push({ - name: tableName, - columns: structure, - foreignKeys, - indexes - }); - } - - return { - success: true, - databaseSchema - }; - } catch (err: any) { - return { - success: false, - message: err.message || 'Error fetching PostgreSQL database schema', - databaseSchema: { tables: [] } - }; - } finally { - await safePgEndConnection(client); - } -} - -// At the end of the registerTablesHandlers function, add this handler -export function registerTablesHandlers() { - // Existing handlers... - - // Add PostgreSQL schema handler - ipcMain.handle( - 'get-postgresql-database-schema-for-ai', - getPostgresqlDatabaseSchemaForAIHandler - ); -} -``` - -### 2. Update Database Schema Service - -Update `src/services/databaseSchema.ts` to support PostgreSQL: - -```typescript -// Add type imports if needed -import { PostgresqlConnection } from '@/types/postgresql-connection'; - -// In the fetchDatabaseSchema function, modify it to handle PostgreSQL: -const fetchDatabaseSchema = async ( - force = false -): Promise => { - if (databaseSchema.value && !force) { - return databaseSchema.value; - } - - const selectedProject = connectionsStore.getSelectedProject; - if (!selectedProject) { - error.value = 'No project selected'; - return null; - } - - isLoading.value = true; - error.value = null; - - try { - let result; - - if (selectedProject.type === 'postgresql') { - result = await window.ipcRenderer.getPostgresqlDatabaseSchemaForAI( - toRaw(selectedProject.db_config) - ); - } else { - // Default to MySQL - result = await window.ipcRenderer.getDatabaseSchemaForAI( - toRaw(selectedProject.db_config) - ); - } - - if (result.success) { - const modelsResult = - await projectStore.getModelsForTables(selectedProject); - const models = modelsResult?.models || []; - - if (result.databaseSchema && result.databaseSchema.tables) { - result.databaseSchema.tables.forEach((table: TableSchema) => { - const model = models.find((m) => m.table === table.name); - if (model) { - table.model = { - name: model.name, - namespace: model.namespace, - fullName: model.fullName - }; - } - }); - } - - databaseSchema.value = result.databaseSchema; - return databaseSchema.value; - } else { - error.value = result.message || 'Failed to get database schema'; - console.error('Failed to get database schema:', result.message); - return null; - } - } catch (err) { - error.value = err instanceof Error ? err.message : String(err); - console.error('Error fetching database schema:', err); - return null; - } finally { - isLoading.value = false; - } -}; -``` - -### 3. Update Preload Script for Schema Functions - -Add this to the preload script (`electron/preload/index.ts`): - -```typescript -// Add this to the existing handlers -getPostgresqlDatabaseSchemaForAI: (config: any) => { - return ipcRenderer.invoke('get-postgresql-database-schema-for-ai', config); -}, -``` - -## Verification - -- Ensure PostgreSQL schema retrieval functions work correctly -- Test schema retrieval with both MySQL and PostgreSQL databases -- Verify foreign key relationships are properly detected -- Check that indexes are correctly displayed - -## Next Steps - -After completing these tasks, proceed to [Step 5: Update SQL Executor](./05-sql-executor.md). diff --git a/docs/postgresql-implementation/05-sql-executor.md b/docs/postgresql-implementation/05-sql-executor.md deleted file mode 100644 index 7eecd67..0000000 --- a/docs/postgresql-implementation/05-sql-executor.md +++ /dev/null @@ -1,175 +0,0 @@ -# Step 5: Update SQL Executor - -In this step, we'll update the SQL execution functionality to support PostgreSQL queries. This involves adding PostgreSQL-specific query execution functions and updating the UI components to use the appropriate functions based on the database type. - -## Tasks - -- [ ] Update SQL Executor module to handle PostgreSQL queries -- [ ] Add IPC handlers for PostgreSQL query execution -- [ ] Update UI components to use PostgreSQL query handlers - -## Implementation Details - -### 1. Update SQL Executor Module - -Update the `electron/modules/sql-executor.ts` file to support PostgreSQL queries: - -```typescript -import { ipcMain } from 'electron'; -import { - createConnection as createMysqlConnection, - releaseConnection as releaseMysqlConnection -} from '../helpers/mysql'; -import { - createConnection as createPgConnection, - releaseConnection as releasePgConnection -} from '../helpers/postgresql'; -import { MysqlConnection } from '../../src/types/mysql-connection'; -import { PostgresqlConnection } from '../../src/types/postgresql-connection'; - -// Rename the existing function to be MySQL-specific -async function executeMysqlQueryHandler( - _: any, - config: MysqlConnection, - query: string -) { - let connection: any; - - try { - connection = await createMysqlConnection(config); - - try { - const [results] = await connection.query(query); - - return { success: true, results }; - } catch (error) { - console.error('Error executing MySQL query:', error.message); - return { success: false, error: error.message }; - } - } catch (error) { - console.error('Error connecting to MySQL database:', error.message); - return { success: false, error: error.message }; - } finally { - if (connection) { - await releaseMysqlConnection(connection); - } - } -} - -// Add a PostgreSQL query executor -async function executePostgresqlQueryHandler( - _: any, - config: PostgresqlConnection, - query: string -) { - let client: any; - - try { - client = await createPgConnection(config); - - try { - const results = await client.query(query); - return { success: true, results: results.rows }; - } catch (error) { - console.error('Error executing PostgreSQL query:', error.message); - return { success: false, error: error.message }; - } - } catch (error) { - console.error( - 'Error connecting to PostgreSQL database:', - error.message - ); - return { success: false, error: error.message }; - } finally { - if (client) { - await releasePgConnection(client); - } - } -} - -// Update the register function to include both handlers -function registerSqlExecutorHandlers() { - // Rename this to be MySQL-specific - ipcMain.handle('execute-mysql-query', executeMysqlQueryHandler); - - // Add PostgreSQL handler - ipcMain.handle('execute-postgresql-query', executePostgresqlQueryHandler); -} - -export { registerSqlExecutorHandlers }; -``` - -### 2. Update Preload Script - -In `electron/preload/index.ts`, add the PostgreSQL SQL execution function: - -```typescript -// Add new handler for PostgreSQL query execution -executePostgresqlQuery: (config: any, query: string) => { - return ipcRenderer.invoke('execute-postgresql-query', config, query); -}, -``` - -### 3. Update SQLEditorView Component - -Modify the `src/views/SQLEditorView.vue` file to handle PostgreSQL queries: - -```typescript -// In the executeQuery function: -async function executeQuery() { - // ...existing code... - - try { - isExecuting.value = true; - errorMessage.value = ''; - - let result; - - // Choose the appropriate handler based on database type - if (project.value.type === 'postgresql') { - result = await window.ipcRenderer.executePostgresqlQuery( - toRaw(project.value.db_config), - query - ); - } else { - // Default to MySQL - result = await window.ipcRenderer.executeMysqlQuery( - toRaw(project.value.db_config), - query - ); - } - - // Process results... - if (result.success) { - // ...existing success handling... - } else { - // ...existing error handling... - } - } catch (error) { - // ...existing error handling... - } finally { - // ...existing cleanup... - } -} -``` - -### 4. Update Any Other Components that Execute SQL - -Look for any other components that directly execute SQL queries and update them similarly. For example, check: - -- Components that display table data -- Components for database operations -- Components for running schema migrations - -For each of these, add conditional logic to use the appropriate handler based on the connection type. - -## Verification - -- Test SQL query execution with both MySQL and PostgreSQL databases -- Verify that query results are properly displayed -- Check that error handling works correctly for both database types -- Test complex queries with joins, aggregations, etc. - -## Next Steps - -After completing these tasks, proceed to [Step 6: Update UI Components](./06-ui-components.md). diff --git a/docs/postgresql-implementation/06-ui-components.md b/docs/postgresql-implementation/06-ui-components.md deleted file mode 100644 index 442543f..0000000 --- a/docs/postgresql-implementation/06-ui-components.md +++ /dev/null @@ -1,348 +0,0 @@ -# Step 6: Update UI Components - -In this step, we'll update the UI components to support PostgreSQL connections and databases. This includes modifying the connection management UI, database view, and other components. - -## Tasks - -- [ ] Update database connection UI to support PostgreSQL -- [ ] Update connection store to handle PostgreSQL connections -- [ ] Add PostgreSQL-specific UI elements and icons -- [ ] Update environment file detection for PostgreSQL - -## Implementation Details - -### 1. Update Connection Management UI - -Modify the `src/components/home/ManageConnection.vue` file to allow selecting PostgreSQL: - -```vue - -
- - -
- - -
- - -
- - -
- - - -
- - -
- -
-``` - -### 2. Update the saveNewConnection Method - -Update the `saveNewConnection` method in `src/components/home/ManageConnection.vue`: - -```typescript -async function saveNewConnection() { - try { - // Existing validation code... - - isSaving.value = true; - - showAlert('Testing database connection...', 'info'); - - let testResult; - - if (newConnection.value.type === 'postgresql') { - // Use PostgreSQL connection test - testResult = await window.ipcRenderer.testPostgreSQLConnection({ - host: newConnection.value.db_config.host, - port: newConnection.value.db_config.port, - user: newConnection.value.db_config.user, - password: newConnection.value.db_config.password, - database: newConnection.value.db_config.database, - schema: newConnection.value.db_config.schema || 'public', - ssl: newConnection.value.db_config.ssl || false - }); - } else { - // Use MySQL connection test - testResult = await window.ipcRenderer.testMySQLConnection({ - host: newConnection.value.db_config.host, - port: newConnection.value.db_config.port, - user: newConnection.value.db_config.user, - password: newConnection.value.db_config.password, - database: newConnection.value.db_config.database - }); - } - - if (!testResult.success) { - showAlert(`Connection failed: ${testResult.message}`, 'error'); - isSaving.value = false; - return; - } - - showAlert('Connection successful! Saving configuration...', 'success'); - - // Create the db_config object based on the database type - let dbConfig; - if (newConnection.value.type === 'postgresql') { - dbConfig = { - database: newConnection.value.db_config.database, - host: newConnection.value.db_config.host, - port: newConnection.value.db_config.port, - user: newConnection.value.db_config.user, - password: newConnection.value.db_config.password, - schema: newConnection.value.db_config.schema || 'public', - ssl: newConnection.value.db_config.ssl || false, - connectTimeout: 10000 - }; - } else { - dbConfig = { - database: newConnection.value.db_config.database, - host: newConnection.value.db_config.host, - port: newConnection.value.db_config.port, - user: newConnection.value.db_config.user, - password: newConnection.value.db_config.password, - connectTimeout: 10000 - }; - } - - const connectionData = { - id: isEditMode.value ? editConnectionId.value : uuid(), - projectPath: newConnection.value.projectPath, - name: newConnection.value.name, - type: newConnection.value.type, - icon: newConnection.value.type.charAt(0).toUpperCase(), - db_config: dbConfig, - redis_config: { - port: newConnection.value.redis_config.port, - host: newConnection.value.redis_config.host, - password: newConnection.value.redis_config.password - }, - usingSail: newConnection.value.usingSail, - status: 'ready', - isValid: true, - dockerInfo: dockerInfo.value || null - }; - - // Existing save logic... - } catch (error: any) { - console.error('Error saving connection:', error); - showAlert(`Error saving connection: ${error.message}`, 'error'); - } finally { - isSaving.value = false; - } -} -``` - -### 3. Update the Connections Store - -Modify `src/store/connections.ts` to handle PostgreSQL connections: - -```typescript -async function loadConnections(id: string | null = null) { - isLoading.value = true; - - // ...existing code... - - try { - if (window.ipcRenderer) { - try { - const savedProjects: ProjectConnection[] = JSON.parse( - localStorage.getItem('connections') || '[]' - ); - - if ( - savedProjects && - Array.isArray(savedProjects) && - savedProjects.length > 0 - ) { - for (const project of savedProjects) { - let check; - - if (project.type === 'postgresql') { - check = await window.ipcRenderer.testPostgreSQLConnection( - toRaw(project.db_config) - ); - } else { - check = await window.ipcRenderer.testMySQLConnection( - toRaw(project.db_config) - ); - } - - project.isValid = check.success; - project.status = check.success - ? 'connected' - : 'disconnected'; - } - - connections.value = savedProjects; - } else { - connections.value = []; - } - } catch (err) { - // ...error handling... - } - } - // ...rest of the function... - } -} -``` - -### 4. Update Database Connection Colors - -Update the connection color function in `src/views/Home.vue` and `src/components/BaseHeader.vue`: - -```typescript -function getConnectionColor(type: string) { - switch (type) { - case 'mysql': - return 'bg-orange-500'; - case 'postgresql': - return 'bg-blue-600'; - default: - return 'bg-gray-600'; - } -} -``` - -### 5. Update Environment File Detection - -Modify the `selectProjectDirectory` function in `src/components/home/ManageConnection.vue` to detect PostgreSQL connections from the Laravel .env file: - -```typescript -async function selectProjectDirectory() { - try { - // ...existing code... - - const envConfig: Env = - await window.ipcRenderer.readEnvFile(selectedPath); - - if (envConfig) { - // Set connection type based on DB_CONNECTION - if (envConfig.DB_CONNECTION) { - if ( - envConfig.DB_CONNECTION.toLowerCase() === 'pgsql' || - envConfig.DB_CONNECTION.toLowerCase() === 'postgres' || - envConfig.DB_CONNECTION.toLowerCase() === 'postgresql' - ) { - newConnection.value.type = 'postgresql'; - // Set default PostgreSQL port - if (!newConnection.value.db_config.port) { - newConnection.value.db_config.port = 5432; - } - } else { - newConnection.value.type = 'mysql'; - // Set default MySQL port - if (!newConnection.value.db_config.port) { - newConnection.value.db_config.port = 3306; - } - } - } - - // ...existing code for setting other connection properties... - } - } catch (error) { - console.error(error); - showAlert('Error selecting project directory', 'error'); - } -} -``` - -### 6. Add PostgreSQL Default Values - -Update the default connection values in `src/components/home/ManageConnection.vue`: - -```typescript -// Update this based on the selected type -const getDefaultValues = (type = 'mysql') => ({ - id: '', - projectPath: '', - name: '', - type: type, - icon: '', - db_config: { - database: '', - host: 'localhost', - port: type === 'postgresql' ? 5432 : 3306, - user: '', - password: '', - schema: type === 'postgresql' ? 'public' : undefined, - ssl: type === 'postgresql' ? false : undefined, - connectTimeout: 10000 - }, - redis_config: { - port: 6379, - host: 'localhost', - password: '' - }, - usingSail: false, - status: 'ready', - isValid: true, - dockerInfo: null -}); - -// Update when type changes -watch( - () => newConnection.value.type, - (newType) => { - // Update default port when database type changes - if (newType === 'postgresql') { - newConnection.value.db_config.port = 5432; - newConnection.value.db_config.schema = 'public'; - newConnection.value.db_config.ssl = false; - } else { - newConnection.value.db_config.port = 3306; - delete newConnection.value.db_config.schema; - delete newConnection.value.db_config.ssl; - } - } -); -``` - -## Verification - -- Test creating new PostgreSQL connections with different settings -- Verify that connection test works correctly with PostgreSQL -- Test loading connections from environment files -- Check that PostgreSQL-specific fields appear and disappear appropriately -- Verify that the UI displays PostgreSQL connections with the correct color - -## Next Steps - -After completing these tasks, proceed to [Step 7: Install Dependencies](./07-dependencies.md). diff --git a/docs/postgresql-implementation/07-dependencies.md b/docs/postgresql-implementation/07-dependencies.md deleted file mode 100644 index 29aee4e..0000000 --- a/docs/postgresql-implementation/07-dependencies.md +++ /dev/null @@ -1,78 +0,0 @@ -# Step 7: Install Dependencies - -In this step, we'll install the required PostgreSQL dependencies to enable PostgreSQL support in the application. - -## Tasks - -- [ ] Install PostgreSQL client library -- [ ] Install PostgreSQL type definitions -- [ ] Update package.json with new dependencies - -## Implementation Details - -### 1. Install PostgreSQL Client Library - -The primary dependency for PostgreSQL support is the `pg` package, which is the official PostgreSQL client for Node.js. Install it by running the following command in your project directory: - -```bash -npm install pg -``` - -### 2. Install PostgreSQL Type Definitions - -For TypeScript support, install the type definitions for the PostgreSQL client: - -```bash -npm install @types/pg --save-dev -``` - -### 3. Update package.json - -Verify that the dependencies were added to your `package.json` file. The relevant sections should now include: - -```json -{ - "dependencies": { - // Existing dependencies... - "pg": "^8.11.0" // Version may vary - // Other dependencies... - }, - "devDependencies": { - // Existing dev dependencies... - "@types/pg": "^8.10.0" // Version may vary - // Other dev dependencies... - } -} -``` - -### 4. Rebuild the Application - -After installing the dependencies, rebuild the application to ensure everything is properly integrated: - -```bash -npm run build -``` - -## Verification - -- Check that the PostgreSQL dependencies are correctly installed -- Verify that there are no compilation errors related to PostgreSQL -- Ensure that the application can import PostgreSQL modules without errors -- Test a basic PostgreSQL connection to verify the library is working - -## Troubleshooting - -If you encounter any issues installing or using the PostgreSQL client library: - -1. Check for compatibility issues between the `pg` package and your Node.js version -2. Ensure that the TypeScript types are correctly recognized by the compiler -3. Verify that the import statements for PostgreSQL modules are correct - -For native module compilation issues (especially on Windows): - -1. Make sure you have the necessary build tools installed -2. Consider using binary distributions if available - -## Next Steps - -After completing these tasks, proceed to [Step 8: Testing & Verification](./08-testing.md). diff --git a/docs/postgresql-implementation/08-testing.md b/docs/postgresql-implementation/08-testing.md deleted file mode 100644 index 67ae20e..0000000 --- a/docs/postgresql-implementation/08-testing.md +++ /dev/null @@ -1,186 +0,0 @@ -# Step 8: Testing & Verification - -In this step, we'll test and verify the PostgreSQL integration to ensure it works correctly with the application. This includes testing connections, queries, schema retrieval, and UI components. - -## Tasks - -- [ ] Set up a PostgreSQL database for testing -- [ ] Test connection functionality -- [ ] Test SQL query execution -- [ ] Test schema retrieval -- [ ] Test UI components -- [ ] Verify integration with Laravel - -## Implementation Details - -### 1. Set Up a PostgreSQL Database for Testing - -First, you need a PostgreSQL database to test with. You can: - -- Install PostgreSQL locally -- Use a Docker container -- Use a cloud-hosted PostgreSQL instance - -#### Local Installation - -If you prefer to install PostgreSQL locally: - -1. Download and install PostgreSQL from the [official website](https://www.postgresql.org/download/) -2. Create a new database for testing: - ```sql - CREATE DATABASE larabase_test; - ``` -3. Create a test user: - ```sql - CREATE USER larabase WITH PASSWORD 'password'; - GRANT ALL PRIVILEGES ON DATABASE larabase_test TO larabase; - ``` - -#### Docker Setup - -If you prefer using Docker: - -```bash -docker run --name postgres-test -e POSTGRES_PASSWORD=password -e POSTGRES_USER=larabase -e POSTGRES_DB=larabase_test -p 5432:5432 -d postgres -``` - -### 2. Test Connection Functionality - -Test PostgreSQL connection by: - -1. Start the Larabase application in development mode: - - ```bash - npm run dev - ``` - -2. Open the application and create a new PostgreSQL connection with the following details: - - - Type: PostgreSQL - - Host: localhost (or your server address) - - Port: 5432 - - Database: larabase_test - - Username: larabase - - Password: password - - Schema: public - -3. Test the connection to verify it works correctly. - -### 3. Test SQL Query Execution - -Test PostgreSQL query execution functionality: - -1. Open the SQL Editor for your PostgreSQL connection -2. Create a test table: - ```sql - CREATE TABLE test_users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL, - email VARCHAR(100) UNIQUE NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - ``` -3. Insert some test data: - ```sql - INSERT INTO test_users (name, email) VALUES - ('Test User 1', 'test1@example.com'), - ('Test User 2', 'test2@example.com'), - ('Test User 3', 'test3@example.com'); - ``` -4. Run a SELECT query: - ```sql - SELECT * FROM test_users; - ``` -5. Verify that results are displayed correctly - -### 4. Test Schema Retrieval - -Test the database schema retrieval: - -1. Open the database view for your PostgreSQL connection -2. Create test tables with relationships: - - ```sql - CREATE TABLE test_categories ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL - ); - - CREATE TABLE test_posts ( - id SERIAL PRIMARY KEY, - title VARCHAR(200) NOT NULL, - content TEXT, - category_id INTEGER REFERENCES test_categories(id), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE test_comments ( - id SERIAL PRIMARY KEY, - content TEXT NOT NULL, - post_id INTEGER REFERENCES test_posts(id), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - ``` - -3. Verify that the tables appear in the schema browser -4. Check that foreign key relationships are correctly displayed -5. Test the ERD visualization to ensure relationships are shown correctly - -### 5. Test UI Components - -Test all UI components with PostgreSQL: - -1. Test table list and content browsing -2. Test data modification (insert, update, delete) -3. Verify that PostgreSQL-specific UI elements (schema selection, etc.) work correctly -4. Test connection editing and updating -5. Verify that PostgreSQL connections are displayed with the correct color and icon - -### 6. Verify Integration with Laravel - -Test integration with a Laravel project that uses PostgreSQL: - -1. Create a new Laravel project or use an existing one -2. Configure it to use PostgreSQL in the `.env` file: - ``` - DB_CONNECTION=pgsql - DB_HOST=127.0.0.1 - DB_PORT=5432 - DB_DATABASE=larabase_test - DB_USERNAME=larabase - DB_PASSWORD=password - ``` -3. Create and run some migrations -4. Connect Larabase to this Laravel project -5. Verify that the migrations and tables are correctly detected -6. Test running Laravel commands through Larabase - -## Additional Testing - -1. **Error Handling**: Test various error scenarios (incorrect credentials, server unavailable, etc.) -2. **Large Datasets**: Test performance with large tables and datasets -3. **Complex Queries**: Test with complex queries, joins, subqueries, etc. -4. **PostgreSQL-Specific Features**: Test PostgreSQL-specific features (JSON fields, arrays, etc.) - -## Troubleshooting Common Issues - -1. **Connection Issues**: - - - Verify PostgreSQL server is running - - Check firewall settings - - Ensure correct hostname/port - -2. **Authentication Issues**: - - - Verify credentials - - Check PostgreSQL authentication configuration (`pg_hba.conf`) - -3. **Schema Issues**: - - Verify schema name is correct - - Check user has permissions to access schema - -## Completing the Implementation - -Once all tests pass and everything works correctly, you have successfully integrated PostgreSQL support into Larabase! - -Update the implementation tracking in the main `postgresql-implementation.md` file to mark all tasks as completed. diff --git a/docs/ssh-implementation.md b/docs/ssh-implementation.md deleted file mode 100644 index fc33c75..0000000 --- a/docs/ssh-implementation.md +++ /dev/null @@ -1,93 +0,0 @@ -# SSH Remote Connection Implementation Guide - -This guide provides a step-by-step process for adding SSH remote connection support to Larabase. SSH connections will be treated as a distinct connection type rather than an optional feature for local connections. - -## Implementation Overview - -Larabase currently supports MySQL and PostgreSQL database connections. This implementation will add support for remote connections via SSH as a completely separate connection type. Remote connections will be clearly differentiated from local connections with badges in the UI. - -## Key Features - -- Connect to remote Laravel projects via SSH -- Browse and edit remote files -- Execute artisan and composer commands remotely -- Connect to remote databases through SSH tunnels -- Clearly distinguish remote connections in the UI - -## Implementation Steps - -1. [Create SSH Connection Types](./ssh-implementation/01-types.md) -2. [Set Up SSH Helper Module](./ssh-implementation/02-helper-module.md) -3. [Create IPC Handlers for SSH](./ssh-implementation/03-ipc-handlers.md) -4. [Implement SSH Tunneling](./ssh-implementation/04-ssh-tunneling.md) -5. [Update UI Components for SSH](./ssh-implementation/05-ui-components.md) -6. [Add Remote Indication Badges](./ssh-implementation/06-remote-badges.md) -7. [Handle Remote File Operations](./ssh-implementation/07-remote-files.md) -8. [Execute Remote Commands](./ssh-implementation/08-remote-commands.md) -9. [Install Dependencies](./ssh-implementation/09-dependencies.md) -10. [Testing & Verification](./ssh-implementation/10-testing.md) - -## Implementation Summary - -### Types and Interfaces - -We'll define new TypeScript interfaces for SSH connections, differentiating them clearly from local database connections. SSH connections will have a separate connection type and will always be marked as remote connections. - -### SSH Functionality - -The implementation includes a comprehensive SSH module using the `ssh2` library to handle connections, file transfers, and command execution. We'll also implement SSH tunneling to allow secure connections to remote databases. - -### UI Components - -The UI will be updated with new components for SSH-specific functionality, including a remote file browser and command executor. We'll also add badges to clearly indicate which connections are remote. - -### Database Integration - -Remote connections will be able to interact with databases through SSH tunnels, allowing all existing database functionality to work with remote connections as well. - -## Development Approach - -This implementation follows a modular, step-by-step approach: - -1. First, we establish the core SSH functionality for connecting to remote servers -2. Then, we implement file operations and command execution -3. Next, we add SSH tunneling for database connections -4. Finally, we update the UI to provide a seamless user experience - -Each step builds on the previous ones, ensuring that the implementation is robust and well-structured. - -## Compatibility Considerations - -The SSH implementation is designed to work with: - -- Both MySQL and PostgreSQL databases -- Various authentication methods (password, private key) -- Different server configurations and environments - -## Resources - -- [SSH2 Library Documentation](https://github.com/mscdex/ssh2) -- [Laravel Documentation](https://laravel.com/docs) -- [Electron Documentation for IPC](https://www.electronjs.org/docs/latest/tutorial/ipc) - -## Implementation Tracking - -Use this section to track your progress through the implementation steps: - -- [ ] Create SSH Connection Types -- [ ] Set Up SSH Helper Module -- [ ] Create IPC Handlers for SSH -- [ ] Implement SSH Tunneling -- [ ] Update UI Components for SSH -- [ ] Add Remote Indication Badges -- [ ] Handle Remote File Operations -- [ ] Execute Remote Commands -- [ ] Install Dependencies -- [ ] Testing & Verification - -## Notes - -- Each markdown file contains detailed instructions and code snippets for the specific implementation step. -- Code samples include file paths indicating where changes should be made. -- Some steps may require additional configuration depending on your development environment. -- SSH connections are treated as a separate connection type, not as an add-on to local connections. diff --git a/docs/ssh-implementation/01-types.md b/docs/ssh-implementation/01-types.md deleted file mode 100644 index d7140d9..0000000 --- a/docs/ssh-implementation/01-types.md +++ /dev/null @@ -1,135 +0,0 @@ -# Step 1: Create SSH Connection Types - -In this step, we'll create the necessary TypeScript type definitions for SSH remote connections. - -## Tasks - -- [ ] Create SSH connection type definition -- [ ] Update project connection type to include SSH as a distinct connection type -- [ ] Define connection type enum for clear type identification - -## Implementation Details - -### 1. Create SSH Connection Type - -Create a new file `src/types/ssh-connection.d.ts` with the following content: - -```typescript -export interface SshConnection { - host: string; - port: number; - username: string; - password?: string; - privateKey?: string; - passphrase?: string; - remotePath: string; - remoteDbType: 'mysql' | 'postgresql'; - remoteDbConfig: { - host: string; - port: number; - database: string; - username: string; - password?: string; - schema?: string; // For PostgreSQL - }; -} -``` - -### 2. Create Connection Type Enum - -Create a new file `src/types/connection-types.ts` to define the connection types: - -```typescript -export enum ConnectionType { - MySQL = 'mysql', - PostgreSQL = 'postgresql', - SSH = 'ssh' -} - -export function getConnectionTypeIcon(type: ConnectionType): string { - switch (type) { - case ConnectionType.MySQL: - return 'M'; - case ConnectionType.PostgreSQL: - return 'P'; - case ConnectionType.SSH: - return 'S'; - default: - return ''; - } -} - -export function getConnectionTypeColor(type: ConnectionType): string { - switch (type) { - case ConnectionType.MySQL: - return 'bg-orange-500'; - case ConnectionType.PostgreSQL: - return 'bg-blue-600'; - case ConnectionType.SSH: - return 'bg-green-600'; - default: - return 'bg-gray-600'; - } -} - -export function getConnectionTypeLabel(type: ConnectionType): string { - switch (type) { - case ConnectionType.MySQL: - return 'MySQL'; - case ConnectionType.PostgreSQL: - return 'PostgreSQL'; - case ConnectionType.SSH: - return 'SSH Remote'; - default: - return 'Unknown'; - } -} - -export function isRemoteConnection(type: ConnectionType): boolean { - return type === ConnectionType.SSH; -} -``` - -### 3. Update Project Connection Type - -Modify the `src/types/project.d.ts` file to integrate the SSH connection type: - -```typescript -import { MysqlConnection } from './mysql-connection'; -import { PostgresqlConnection } from './postgresql-connection'; -import { SshConnection } from './ssh-connection'; -import { RedisConnection } from './redis'; -import { DockerInfo } from './docker-info'; -import { ConnectionType } from './connection-types'; - -export interface ProjectConnection { - id?: string | null; - name: string; - projectPath: string; - type: ConnectionType; // Change to enum type - icon: string | undefined | null; - isRemote: boolean; // Flag to indicate if the connection is remote - - // One of these will be used depending on the connection type - db_config?: MysqlConnection | PostgresqlConnection; - ssh_config?: SshConnection; - - redis_config: RedisConnection; - usingSail: boolean; - status: string; - isValid: boolean; - dockerInfo?: DockerInfo | undefined | null; -} - -// Rest of the file remains unchanged -``` - -## Verification - -- Confirm all type definition files have been created/updated correctly -- Ensure there are no TypeScript errors when building the project -- Verify imports resolve correctly - -## Next Steps - -After completing these tasks, proceed to [Step 2: Set Up SSH Helper Module](./02-helper-module.md). diff --git a/docs/ssh-implementation/02-helper-module.md b/docs/ssh-implementation/02-helper-module.md deleted file mode 100644 index 8037c24..0000000 --- a/docs/ssh-implementation/02-helper-module.md +++ /dev/null @@ -1,330 +0,0 @@ -# Step 2: Set Up SSH Helper Module - -In this step, we'll create a helper module to manage SSH connections. This will handle establishing SSH connections, executing commands, and transferring files via SFTP. - -## Tasks - -- [ ] Install SSH2 client library -- [ ] Create SSH connection helper module -- [ ] Implement connection management functions -- [ ] Implement remote command execution -- [ ] Implement SFTP file operations - -## Implementation Details - -### 1. Install SSH2 Client Library - -First, install the required SSH client library: - -```bash -npm install ssh2 -npm install @types/ssh2 --save-dev -``` - -### 2. Create SSH Helper Module - -Create a new file `electron/helpers/ssh.ts` with the following content: - -```typescript -import { Client, SFTPWrapper, ClientChannel } from 'ssh2'; -import * as fs from 'fs'; -import * as path from 'path'; -import { SshConnection } from '../../src/types/ssh-connection'; - -// Map to store active SSH connections -const sshConnections = new Map(); - -// Get a unique key for a connection -function getConnectionKey(config: SshConnection): string { - return `${config.username}@${config.host}:${config.port}:${config.remotePath}`; -} - -// Create a new SSH connection -async function createConnection(config: SshConnection): Promise { - return new Promise((resolve, reject) => { - const conn = new Client(); - - // Configure SSH connection - const connectConfig: any = { - host: config.host, - port: config.port, - username: config.username - }; - - // Use private key or password for authentication - if (config.privateKey) { - connectConfig.privateKey = fs.readFileSync(config.privateKey); - if (config.passphrase) { - connectConfig.passphrase = config.passphrase; - } - } else if (config.password) { - connectConfig.password = config.password; - } - - // Set up connection events - conn.on('ready', () => { - // Store connection in the pool - const key = getConnectionKey(config); - sshConnections.set(key, conn); - resolve(conn); - }); - - conn.on('error', (err) => { - reject(err); - }); - - // Connect to SSH server - conn.connect(connectConfig); - }); -} - -// Get an existing connection or create a new one -async function getConnection(config: SshConnection): Promise { - const key = getConnectionKey(config); - - if (sshConnections.has(key)) { - const conn = sshConnections.get(key)!; - - // Check if connection is still active - if (conn.state === 'authenticated') { - return conn; - } else { - // Close the stale connection - sshConnections.delete(key); - try { - conn.end(); - } catch (e) { - /* ignore */ - } - } - } - - // Create a new connection - return await createConnection(config); -} - -// Get an SFTP session -async function getSftpSession(client: Client): Promise { - return new Promise((resolve, reject) => { - client.sftp((err, sftp) => { - if (err) reject(err); - else resolve(sftp); - }); - }); -} - -// Execute a command on the remote server -async function executeCommand( - config: SshConnection, - command: string -): Promise<{ stdout: string; stderr: string; code: number | null }> { - const client = await getConnection(config); - - return new Promise((resolve, reject) => { - client.exec(command, (err, stream) => { - if (err) { - reject(err); - return; - } - - let stdout = ''; - let stderr = ''; - - stream.on('data', (data) => { - stdout += data.toString(); - }); - - stream.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - stream.on('close', (code) => { - resolve({ stdout, stderr, code }); - }); - - stream.on('error', (err) => { - reject(err); - }); - }); - }); -} - -// Read a file from the remote server -async function readRemoteFile( - config: SshConnection, - filePath: string -): Promise { - const client = await getConnection(config); - const sftp = await getSftpSession(client); - - return new Promise((resolve, reject) => { - sftp.readFile(filePath, (err, data) => { - if (err) reject(err); - else resolve(data); - }); - }); -} - -// Write a file to the remote server -async function writeRemoteFile( - config: SshConnection, - filePath: string, - data: Buffer | string -): Promise { - const client = await getConnection(config); - const sftp = await getSftpSession(client); - - return new Promise((resolve, reject) => { - sftp.writeFile(filePath, data, (err) => { - if (err) reject(err); - else resolve(); - }); - }); -} - -// List files in a remote directory -async function listRemoteFiles( - config: SshConnection, - dirPath: string -): Promise { - const client = await getConnection(config); - const sftp = await getSftpSession(client); - - return new Promise((resolve, reject) => { - sftp.readdir(dirPath, (err, list) => { - if (err) reject(err); - else resolve(list); - }); - }); -} - -// Close an SSH connection -function closeConnection(config: SshConnection): void { - const key = getConnectionKey(config); - - if (sshConnections.has(key)) { - const conn = sshConnections.get(key)!; - conn.end(); - sshConnections.delete(key); - } -} - -// Close all SSH connections -function closeAllConnections(): void { - for (const conn of sshConnections.values()) { - try { - conn.end(); - } catch (e) { - /* ignore */ - } - } - sshConnections.clear(); -} - -// Test SSH connection -async function testConnection( - config: SshConnection -): Promise<{ success: boolean; message: string }> { - let client: Client | null = null; - - try { - client = await createConnection(config); - - // Verify we can access the remote path - const testResult = await executeCommand( - config, - `cd ${config.remotePath} && ls -la` - ); - - if (testResult.code !== 0) { - return { - success: false, - message: `Error accessing remote path: ${testResult.stderr}` - }; - } - - // Check if it's a Laravel project - const laravelCheckResult = await executeCommand( - config, - `cd ${config.remotePath} && [ -f artisan ] && echo "Laravel" || echo "Not Laravel"` - ); - - if (laravelCheckResult.stdout.trim() !== 'Laravel') { - return { - success: false, - message: - 'The remote path does not appear to be a Laravel project (no artisan file found)' - }; - } - - return { - success: true, - message: 'Successfully connected to remote Laravel project' - }; - } catch (error) { - return { - success: false, - message: - error instanceof Error - ? error.message - : 'Unknown error connecting to SSH server' - }; - } finally { - if (client) { - closeConnection(config); - } - } -} - -export { - createConnection, - getConnection, - getSftpSession, - executeCommand, - readRemoteFile, - writeRemoteFile, - listRemoteFiles, - closeConnection, - closeAllConnections, - testConnection -}; -``` - -### 3. Update Electron's Main Process Cleanup - -In `electron/main/index.ts`, update the cleanup function to close SSH connections: - -```typescript -// Import SSH helper -import { closeAllConnections } from '../helpers/ssh'; - -// In app.on('window-all-closed') handler: -app.on('window-all-closed', () => { - win = null; - - cleanup(); - - // Close all database connection pools - closeAllPools() - .catch((err) => { - console.error('Error closing database pools:', err); - }) - .finally(() => { - // Close all SSH connections - closeAllConnections(); - - if (process.platform === 'darwin') app.quit(); - }); -}); -``` - -## Verification - -- Ensure SSH2 client library is installed correctly -- Verify the helper module compiles without errors -- Check that the connection management functions work as expected -- Verify that command execution and file operations are properly implemented - -## Next Steps - -After completing these tasks, proceed to [Step 3: Create IPC Handlers for SSH](./03-ipc-handlers.md). diff --git a/docs/ssh-implementation/03-ipc-handlers.md b/docs/ssh-implementation/03-ipc-handlers.md deleted file mode 100644 index 13e548f..0000000 --- a/docs/ssh-implementation/03-ipc-handlers.md +++ /dev/null @@ -1,272 +0,0 @@ -# Step 3: Create IPC Handlers for SSH - -In this step, we'll create IPC handlers to allow the renderer process (UI) to interact with the SSH functionality in the main process. These handlers will expose SSH-related operations to the frontend. - -## Tasks - -- [ ] Create SSH module for IPC handlers -- [ ] Register SSH IPC handlers in the main process -- [ ] Update preload script to expose SSH API -- [ ] Add type definitions for the SSH API - -## Implementation Details - -### 1. Create SSH IPC Handlers Module - -Create a new file `electron/modules/ssh.ts` with the following content: - -```typescript -import { ipcMain } from 'electron'; -import * as path from 'path'; -import { - testConnection, - createConnection, - closeConnection, - executeCommand, - readRemoteFile, - writeRemoteFile, - listRemoteFiles -} from '../helpers/ssh'; -import { SshConnection } from '../../src/types/ssh-connection'; - -function registerSshHandlers() { - // Test SSH connection - ipcMain.handle('ssh:test-connection', async (_, config: SshConnection) => { - return await testConnection(config); - }); - - // Execute a command on the remote server - ipcMain.handle( - 'ssh:execute-command', - async (_, config: SshConnection, command: string) => { - return await executeCommand(config, command); - } - ); - - // Read a file from the remote server - ipcMain.handle( - 'ssh:read-file', - async (_, config: SshConnection, filePath: string) => { - try { - const content = await readRemoteFile(config, filePath); - return { success: true, content: content.toString('utf-8') }; - } catch (error) { - return { - success: false, - error: - error instanceof Error - ? error.message - : 'Unknown error reading file' - }; - } - } - ); - - // Write a file to the remote server - ipcMain.handle( - 'ssh:write-file', - async (_, config: SshConnection, filePath: string, content: string) => { - try { - await writeRemoteFile(config, filePath, content); - return { success: true }; - } catch (error) { - return { - success: false, - error: - error instanceof Error - ? error.message - : 'Unknown error writing file' - }; - } - } - ); - - // List files in a directory on the remote server - ipcMain.handle( - 'ssh:list-files', - async (_, config: SshConnection, dirPath: string) => { - try { - const files = await listRemoteFiles(config, dirPath); - return { success: true, files }; - } catch (error) { - return { - success: false, - error: - error instanceof Error - ? error.message - : 'Unknown error listing files' - }; - } - } - ); - - // Get Laravel environment variables - ipcMain.handle('ssh:get-env', async (_, config: SshConnection) => { - try { - const envPath = path.join(config.remotePath, '.env'); - const content = await readRemoteFile(config, envPath); - - return { success: true, content: content.toString('utf-8') }; - } catch (error) { - return { - success: false, - error: - error instanceof Error - ? error.message - : 'Unknown error reading .env file' - }; - } - }); - - // Update Laravel environment variables - ipcMain.handle( - 'ssh:update-env', - async (_, config: SshConnection, content: string) => { - try { - const envPath = path.join(config.remotePath, '.env'); - await writeRemoteFile(config, envPath, content); - - return { success: true }; - } catch (error) { - return { - success: false, - error: - error instanceof Error - ? error.message - : 'Unknown error updating .env file' - }; - } - } - ); - - // Close SSH connection - ipcMain.handle('ssh:close-connection', (_, config: SshConnection) => { - closeConnection(config); - return { success: true }; - }); -} - -export { registerSshHandlers }; -``` - -### 2. Update Main Process to Register SSH Handlers - -Update `electron/main/index.ts` to register the SSH handlers: - -```typescript -import { registerSshHandlers } from '../modules/ssh'; - -// Inside createWindow function, after registering other handlers: -registerSshHandlers(); -``` - -### 3. Update Preload Script - -Update the preload script at `electron/preload/index.ts` to expose the SSH API: - -```typescript -// Add SSH API to contextBridge -contextBridge.exposeInMainWorld('electronAPI', { - // ... existing API - - ssh: { - testConnection: (config: any) => - ipcRenderer.invoke('ssh:test-connection', config), - executeCommand: (config: any, command: string) => - ipcRenderer.invoke('ssh:execute-command', config, command), - readFile: (config: any, filePath: string) => - ipcRenderer.invoke('ssh:read-file', config, filePath), - writeFile: (config: any, filePath: string, content: string) => - ipcRenderer.invoke('ssh:write-file', config, filePath, content), - listFiles: (config: any, dirPath: string) => - ipcRenderer.invoke('ssh:list-files', config, dirPath), - getEnv: (config: any) => ipcRenderer.invoke('ssh:get-env', config), - updateEnv: (config: any, content: string) => - ipcRenderer.invoke('ssh:update-env', config, content), - closeConnection: (config: any) => - ipcRenderer.invoke('ssh:close-connection', config) - } -}); -``` - -### 4. Add Type Definitions for the SSH API - -Update the renderer process type definitions in `src/types/electron-api.d.ts` (create if it doesn't exist): - -```typescript -import { SshConnection } from './ssh-connection'; - -interface ElectronAPI { - // ... existing API types - - ssh: { - testConnection: ( - config: SshConnection - ) => Promise<{ success: boolean; message: string }>; - executeCommand: ( - config: SshConnection, - command: string - ) => Promise<{ - stdout: string; - stderr: string; - code: number | null; - }>; - readFile: ( - config: SshConnection, - filePath: string - ) => Promise<{ - success: boolean; - content?: string; - error?: string; - }>; - writeFile: ( - config: SshConnection, - filePath: string, - content: string - ) => Promise<{ - success: boolean; - error?: string; - }>; - listFiles: ( - config: SshConnection, - dirPath: string - ) => Promise<{ - success: boolean; - files?: any[]; - error?: string; - }>; - getEnv: (config: SshConnection) => Promise<{ - success: boolean; - content?: string; - error?: string; - }>; - updateEnv: ( - config: SshConnection, - content: string - ) => Promise<{ - success: boolean; - error?: string; - }>; - closeConnection: ( - config: SshConnection - ) => Promise<{ success: boolean }>; - }; -} - -declare global { - interface Window { - electronAPI: ElectronAPI; - } -} -``` - -## Verification - -- Ensure all IPC handlers are correctly registered -- Verify that the preload script exposes the SSH API correctly -- Check that the type definitions are accurate and provide good intellisense -- Confirm there are no TypeScript errors - -## Next Steps - -After completing these tasks, proceed to [Step 4: Implement SSH Tunneling](./04-ssh-tunneling.md). diff --git a/docs/ssh-implementation/04-ssh-tunneling.md b/docs/ssh-implementation/04-ssh-tunneling.md deleted file mode 100644 index a346a55..0000000 --- a/docs/ssh-implementation/04-ssh-tunneling.md +++ /dev/null @@ -1,414 +0,0 @@ -# Step 4: Implement SSH Tunneling - -In this step, we'll implement SSH tunneling to allow the application to connect to database servers on the remote machine. This is essential for accessing remote database servers through the SSH connection. - -## Tasks - -- [ ] Extend SSH helper module with tunneling functions -- [ ] Create tunnel management system -- [ ] Add IPC handlers for tunnel operations -- [ ] Implement database connection through SSH tunnels - -## Implementation Details - -### 1. Extend SSH Helper Module - -Update the `electron/helpers/ssh.ts` file to add tunneling functionality: - -```typescript -import { Client, SFTPWrapper, ClientChannel, ForwardedTcpip } from 'ssh2'; -import * as net from 'net'; -import * as crypto from 'crypto'; - -// Add to existing imports and code - -// Map to keep track of active tunnels -const activeTunnels = new Map< - string, - { - server: net.Server; - localPort: number; - remoteHost: string; - remotePort: number; - sshClient: Client; - } ->(); - -// Generate a unique ID for each tunnel -function generateTunnelId(): string { - return crypto.randomBytes(16).toString('hex'); -} - -// Create an SSH tunnel -async function createTunnel( - config: SshConnection, - remoteHost: string, - remotePort: number, - localPort: number = 0 // 0 means use a random available port -): Promise<{ tunnelId: string; localPort: number }> { - const sshClient = await getConnection(config); - - return new Promise((resolve, reject) => { - // Create a local server - const server = net.createServer((socket) => { - // When a connection is made to the local server - sshClient.forwardOut( - '127.0.0.1', // srcIP - socket.localPort || 0, // srcPort - remoteHost, // dstIP - remotePort, // dstPort - (err, stream) => { - if (err) { - socket.end(); - console.error('SSH tunnel error:', err); - return; - } - - // Pipe the SSH stream to the local socket and vice versa - socket.pipe(stream); - stream.pipe(socket); - - stream.on('close', () => { - socket.end(); - }); - - socket.on('close', () => { - stream.end(); - }); - } - ); - }); - - // Handle server errors - server.on('error', (err) => { - reject(err); - }); - - // Listen on the specified local port or a random available port - server.listen(localPort, '127.0.0.1', () => { - const serverAddress = server.address() as net.AddressInfo; - const tunnelId = generateTunnelId(); - - // Store the tunnel information - activeTunnels.set(tunnelId, { - server, - localPort: serverAddress.port, - remoteHost, - remotePort, - sshClient - }); - - // Resolve with the tunnel ID and local port - resolve({ - tunnelId, - localPort: serverAddress.port - }); - }); - }); -} - -// Close an SSH tunnel -function closeTunnel(tunnelId: string): boolean { - if (activeTunnels.has(tunnelId)) { - const tunnel = activeTunnels.get(tunnelId)!; - - // Close the local server - tunnel.server.close(); - activeTunnels.delete(tunnelId); - - return true; - } - - return false; -} - -// Close all SSH tunnels -function closeAllTunnels(): void { - for (const tunnel of activeTunnels.values()) { - tunnel.server.close(); - } - - activeTunnels.clear(); -} - -// Update the cleanup function -function closeAllConnections(): void { - // Close all tunnels first - closeAllTunnels(); - - // Then close all SSH connections - for (const conn of sshConnections.values()) { - try { - conn.end(); - } catch (e) { - /* ignore */ - } - } - - sshConnections.clear(); -} - -// Export the new functions -export { - // Existing exports - createConnection, - getConnection, - getSftpSession, - executeCommand, - readRemoteFile, - writeRemoteFile, - listRemoteFiles, - closeConnection, - closeAllConnections, - testConnection, - - // New tunnel functions - createTunnel, - closeTunnel, - closeAllTunnels -}; -``` - -### 2. Add IPC Handlers for Tunnel Operations - -Update the `electron/modules/ssh.ts` file to add handlers for tunnel operations: - -```typescript -import { - // Existing imports - testConnection, - createConnection, - closeConnection, - executeCommand, - readRemoteFile, - writeRemoteFile, - listRemoteFiles, - - // New tunnel functions - createTunnel, - closeTunnel -} from '../helpers/ssh'; - -// Inside the registerSshHandlers function, add these handlers: - -// Create an SSH tunnel -ipcMain.handle( - 'ssh:create-tunnel', - async ( - _, - config: SshConnection, - remoteHost: string, - remotePort: number, - localPort?: number - ) => { - try { - const result = await createTunnel( - config, - remoteHost, - remotePort, - localPort - ); - return { - success: true, - tunnelId: result.tunnelId, - localPort: result.localPort - }; - } catch (error) { - return { - success: false, - error: - error instanceof Error - ? error.message - : 'Unknown error creating tunnel' - }; - } - } -); - -// Close an SSH tunnel -ipcMain.handle('ssh:close-tunnel', (_, tunnelId: string) => { - const success = closeTunnel(tunnelId); - return { success }; -}); -``` - -### 3. Update Preload Script - -Update the preload script at `electron/preload/index.ts` to expose the tunnel API: - -```typescript -// Inside the ssh object in contextBridge.exposeInMainWorld -ssh: { - // Existing functions - testConnection: (config: any) => ipcRenderer.invoke('ssh:test-connection', config), - executeCommand: (config: any, command: string) => ipcRenderer.invoke('ssh:execute-command', config, command), - // ... other existing functions - - // New tunnel functions - createTunnel: (config: any, remoteHost: string, remotePort: number, localPort?: number) => - ipcRenderer.invoke('ssh:create-tunnel', config, remoteHost, remotePort, localPort), - closeTunnel: (tunnelId: string) => ipcRenderer.invoke('ssh:close-tunnel', tunnelId) -} -``` - -### 4. Update Type Definitions - -Update the type definitions in `src/types/electron-api.d.ts`: - -```typescript -// Inside the ssh object in the ElectronAPI interface -ssh: { - // Existing function types - testConnection: (config: SshConnection) => - Promise<{ success: boolean; message: string }>; - // ... other existing function types - - // New tunnel function types - createTunnel: ( - config: SshConnection, - remoteHost: string, - remotePort: number, - localPort?: number - ) => - Promise<{ - success: boolean; - tunnelId?: string; - localPort?: number; - error?: string; - }>; - closeTunnel: (tunnelId: string) => Promise<{ success: boolean }>; -} -``` - -### 5. Create a Tunnel Service - -Create a new file `src/services/tunnel-service.ts` to manage SSH tunnels: - -```typescript -import { SshConnection } from '../types/ssh-connection'; - -// Store active tunnels -const activeTunnels = new Map< - string, - { - tunnelId: string; - localPort: number; - remoteHost: string; - remotePort: number; - connectionId: string; - } ->(); - -/** - * Create an SSH tunnel to a remote database server - */ -export async function createDatabaseTunnel( - connectionId: string, - sshConfig: SshConnection, - remoteHost: string, - remotePort: number -): Promise<{ tunnelId: string; localPort: number }> { - // Check if a tunnel already exists for this connection - for (const tunnel of activeTunnels.values()) { - if ( - tunnel.connectionId === connectionId && - tunnel.remoteHost === remoteHost && - tunnel.remotePort === remotePort - ) { - return { - tunnelId: tunnel.tunnelId, - localPort: tunnel.localPort - }; - } - } - - // Create a new tunnel - const result = await window.electronAPI.ssh.createTunnel( - sshConfig, - remoteHost, - remotePort - ); - - if (!result.success) { - throw new Error(result.error || 'Failed to create SSH tunnel'); - } - - // Store the tunnel information - const tunnelKey = `${connectionId}:${remoteHost}:${remotePort}`; - activeTunnels.set(tunnelKey, { - tunnelId: result.tunnelId!, - localPort: result.localPort!, - remoteHost, - remotePort, - connectionId - }); - - return { - tunnelId: result.tunnelId!, - localPort: result.localPort! - }; -} - -/** - * Close an SSH tunnel - */ -export async function closeDatabaseTunnel( - connectionId: string, - remoteHost: string, - remotePort: number -): Promise { - const tunnelKey = `${connectionId}:${remoteHost}:${remotePort}`; - - if (activeTunnels.has(tunnelKey)) { - const tunnel = activeTunnels.get(tunnelKey)!; - const result = await window.electronAPI.ssh.closeTunnel( - tunnel.tunnelId - ); - - if (result.success) { - activeTunnels.delete(tunnelKey); - } - - return result.success; - } - - return false; -} - -/** - * Close all SSH tunnels for a specific connection - */ -export async function closeAllConnectionTunnels( - connectionId: string -): Promise { - // Find all tunnels for this connection - for (const [key, tunnel] of activeTunnels.entries()) { - if (tunnel.connectionId === connectionId) { - await window.electronAPI.ssh.closeTunnel(tunnel.tunnelId); - activeTunnels.delete(key); - } - } -} - -/** - * Close all active SSH tunnels - */ -export async function closeAllTunnels(): Promise { - for (const tunnel of activeTunnels.values()) { - await window.electronAPI.ssh.closeTunnel(tunnel.tunnelId); - } - - activeTunnels.clear(); -} -``` - -## Verification - -- Ensure the SSH tunneling functions work correctly -- Test creating tunnels to remote database servers -- Verify that tunnels are properly cleaned up when no longer needed -- Confirm that connections through tunnels work as expected - -## Next Steps - -After completing these tasks, proceed to [Step 5: Update UI Components for SSH](./05-ui-components.md). diff --git a/docs/ssh-implementation/05-ui-components.md b/docs/ssh-implementation/05-ui-components.md deleted file mode 100644 index 52c132b..0000000 --- a/docs/ssh-implementation/05-ui-components.md +++ /dev/null @@ -1,603 +0,0 @@ -# Step 5: Update UI Components for SSH - -In this step, we'll update the UI components to support SSH remote connections. This includes modifying the connection management UI to allow creating and editing SSH connections. - -## Tasks - -- [ ] Update connection management UI for SSH -- [ ] Create a dedicated SSH connection form -- [ ] Update connection store to handle SSH connections -- [ ] Add connection type selector -- [ ] Update project connection list to display SSH connections - -## Implementation Details - -### 1. Update Connection Type Selector - -First, update the connection type selector in `src/components/home/ManageConnection.vue` to include SSH as an option: - -```vue - - - -``` - -### 2. Create SSH Connection Form - -Create a new file `src/components/home/SshConnectionForm.vue` with the following content: - -```vue - - - -``` - -### 3. Update ManageConnection.vue to Include SSH Form - -Update the main connection form in `src/components/home/ManageConnection.vue` to conditionally show the SSH form: - -```vue - - - -``` - -### 4. Update Project List Item to Show Connection Type Badge - -Modify the `src/components/home/ProjectItem.vue` component to show a badge indicating the connection type: - -```vue - - - -``` - -### 5. Update Connection Store - -Update the connection store at `src/store/connections.ts` to handle SSH connections: - -```typescript -// ... existing imports ... -import { ConnectionType } from '../types/connection-types'; -import type { SshConnection } from '../types/ssh-connection'; - -// ... existing code ... - -// Save a connection to IndexedDB -export async function saveConnection( - connection: ProjectConnection -): Promise { - try { - connection.status = 'saved'; - - // Handle special cases for SSH connections - if (connection.type === ConnectionType.SSH) { - // Ensure isRemote is set - connection.isRemote = true; - - // Validate SSH config - if (!connection.ssh_config) { - throw new Error( - 'SSH configuration is required for SSH connections' - ); - } - } else { - // Non-SSH connections should not have SSH config - connection.ssh_config = undefined; - - // Ensure isRemote is false for non-SSH connections - connection.isRemote = false; - } - - // ... rest of existing code ... - } catch (error) { - console.error('Error saving connection:', error); - throw error; - } -} - -// ... rest of existing code ... -``` - -## Verification - -- Ensure the SSH connection form displays all necessary fields -- Verify that the connection type selector works correctly -- Test creating and editing SSH connections -- Confirm that the connection badge displays correctly -- Check that the connection type is properly stored and retrieved - -## Next Steps - -After completing these tasks, proceed to [Step 6: Add Remote Indication Badges](./06-remote-badges.md). diff --git a/docs/ssh-implementation/06-remote-badges.md b/docs/ssh-implementation/06-remote-badges.md deleted file mode 100644 index 3baa10f..0000000 --- a/docs/ssh-implementation/06-remote-badges.md +++ /dev/null @@ -1,441 +0,0 @@ -# Step 6: Add Remote Indication Badges - -In this step, we'll enhance the UI to clearly indicate which projects or database connections are remote versus local. This will be implemented using badges and visual indicators throughout the application. - -## Tasks - -- [ ] Create a reusable remote badge component -- [ ] Update project list to display remote badges -- [ ] Add remote indicators to database tables view -- [ ] Add remote indicators to database connection details -- [ ] Update the project dashboard to show remote status - -## Implementation Details - -### 1. Create a Reusable Remote Badge Component - -Create a new file `src/components/ui/RemoteBadge.vue` with the following content: - -```vue - - - -``` - -### 2. Update Project List Component - -Update the project list item in `src/components/home/ProjectItem.vue` to use the new remote badge component: - -```vue - - - -``` - -### 3. Update Database Tables View - -Update the database tables view in `src/views/database/Tables.vue` to show remote indicators: - -```vue - - - -``` - -### 4. Update Database Connection Details - -Update the database connection details view in `src/components/database/ConnectionInfo.vue` to show remote status: - -```vue - - - -``` - -### 5. Update Project Dashboard - -Update the project dashboard in `src/views/project/Dashboard.vue` to display remote status: - -```vue - - - -``` - -### 6. Update Navigation Component - -Update the application sidebar or navigation component to indicate remote connections: - -```vue - - - -``` - -## Verification - -- Ensure the remote badge component is created and works correctly -- Verify that remote badges appear on all relevant screens -- Confirm that the project list shows remote badges correctly -- Check that the database views properly indicate remote connections -- Test that all remote connection details are displayed correctly - -## Next Steps - -After completing these tasks, proceed to [Step 7: Handle Remote File Operations](./07-remote-files.md). diff --git a/docs/ssh-implementation/07-remote-files.md b/docs/ssh-implementation/07-remote-files.md deleted file mode 100644 index 549f674..0000000 --- a/docs/ssh-implementation/07-remote-files.md +++ /dev/null @@ -1,865 +0,0 @@ -# Step 7: Handle Remote File Operations - -In this step, we'll implement functionality to handle file operations on remote servers via SSH. This will allow users to view, edit, and manage files in their remote Laravel projects. - -## Tasks - -- [ ] Create a file service for remote operations -- [ ] Implement file listing functionality -- [ ] Implement file reading and writing -- [ ] Update environment file editor to work with remote files -- [ ] Create a remote file browser component - -## Implementation Details - -### 1. Create a Remote File Service - -Create a new file `src/services/remote-file-service.ts` to handle remote file operations: - -```typescript -import { SshConnection } from '../types/ssh-connection'; - -/** - * List files in a directory on the remote server - */ -export async function listRemoteFiles( - sshConfig: SshConnection, - directoryPath: string -): Promise< - Array<{ - name: string; - type: 'file' | 'directory'; - size: number; - modifyTime: Date; - }> -> { - try { - const result = await window.electronAPI.ssh.listFiles( - sshConfig, - directoryPath - ); - - if (!result.success) { - throw new Error(result.error || 'Failed to list remote files'); - } - - return result.files.map((file: any) => ({ - name: file.filename, - type: file.longname.startsWith('d') ? 'directory' : 'file', - size: parseInt(file.attrs.size), - modifyTime: new Date(file.attrs.mtime * 1000) - })); - } catch (error) { - console.error('Error listing remote files:', error); - throw error; - } -} - -/** - * Read a file from the remote server - */ -export async function readRemoteFile( - sshConfig: SshConnection, - filePath: string -): Promise { - try { - const result = await window.electronAPI.ssh.readFile( - sshConfig, - filePath - ); - - if (!result.success) { - throw new Error(result.error || 'Failed to read remote file'); - } - - return result.content; - } catch (error) { - console.error('Error reading remote file:', error); - throw error; - } -} - -/** - * Write to a file on the remote server - */ -export async function writeRemoteFile( - sshConfig: SshConnection, - filePath: string, - content: string -): Promise { - try { - const result = await window.electronAPI.ssh.writeFile( - sshConfig, - filePath, - content - ); - - if (!result.success) { - throw new Error(result.error || 'Failed to write remote file'); - } - } catch (error) { - console.error('Error writing remote file:', error); - throw error; - } -} - -/** - * Check if a file exists on the remote server - */ -export async function checkRemoteFileExists( - sshConfig: SshConnection, - filePath: string -): Promise { - try { - const command = `test -f "${filePath}" && echo "exists" || echo "not exists"`; - const result = await window.electronAPI.ssh.executeCommand( - sshConfig, - command - ); - - return result.stdout.trim() === 'exists'; - } catch (error) { - console.error('Error checking if remote file exists:', error); - return false; - } -} - -/** - * Get Laravel environment variables from a remote server - */ -export async function getRemoteEnvVariables( - sshConfig: SshConnection -): Promise { - try { - const envPath = `${sshConfig.remotePath}/.env`; - return await readRemoteFile(sshConfig, envPath); - } catch (error) { - console.error('Error getting remote env variables:', error); - throw error; - } -} - -/** - * Update Laravel environment variables on a remote server - */ -export async function updateRemoteEnvVariables( - sshConfig: SshConnection, - content: string -): Promise { - try { - const envPath = `${sshConfig.remotePath}/.env`; - await writeRemoteFile(sshConfig, envPath, content); - } catch (error) { - console.error('Error updating remote env variables:', error); - throw error; - } -} -``` - -### 2. Create a Remote File Browser Component - -Create a new file `src/components/project/RemoteFileBrowser.vue` to provide a UI for browsing remote files: - -```vue - - - -``` - -### 3. Update Environment File Editor for Remote Files - -Update the environment file editor in `src/components/environment/EnvEditor.vue` to support remote environment files: - -```vue - - - -``` - -### 4. Add Remote File Browser to Project Dashboard - -Update the project dashboard in `src/views/project/Dashboard.vue` to include the remote file browser component: - -```vue - - - -``` - -## Verification - -- Ensure remote file operations work correctly -- Test listing files from remote servers -- Verify reading and writing files works reliably -- Check that the environment file editor properly handles remote files -- Test the remote file browser component functionality - -## Next Steps - -After completing these tasks, proceed to [Step 8: Execute Remote Commands](./08-remote-commands.md). diff --git a/docs/ssh-implementation/08-remote-commands.md b/docs/ssh-implementation/08-remote-commands.md deleted file mode 100644 index 3c7b086..0000000 --- a/docs/ssh-implementation/08-remote-commands.md +++ /dev/null @@ -1,732 +0,0 @@ -# Step 8: Execute Remote Commands - -In this step, we'll implement the ability to execute commands on the remote server. This will allow users to run Laravel artisan commands, migrations, and other CLI tasks on the remote project. - -## Tasks - -- [ ] Create a remote command service -- [ ] Implement artisan command execution -- [ ] Create a command execution UI component -- [ ] Add composer package management -- [ ] Integrate remote commands with database migrations - -## Implementation Details - -### 1. Create a Remote Command Service - -Create a new file `src/services/remote-command-service.ts` to handle command execution: - -```typescript -import { SshConnection } from '../types/ssh-connection'; - -/** - * Execute a command on the remote server - */ -export async function executeRemoteCommand( - sshConfig: SshConnection, - command: string -): Promise<{ - stdout: string; - stderr: string; - code: number | null; -}> { - try { - return await window.electronAPI.ssh.executeCommand(sshConfig, command); - } catch (error) { - console.error('Error executing remote command:', error); - throw error; - } -} - -/** - * Execute an artisan command on the remote server - */ -export async function executeArtisanCommand( - sshConfig: SshConnection, - command: string -): Promise<{ - stdout: string; - stderr: string; - code: number | null; -}> { - const fullCommand = `cd ${sshConfig.remotePath} && php artisan ${command}`; - return await executeRemoteCommand(sshConfig, fullCommand); -} - -/** - * Execute a composer command on the remote server - */ -export async function executeComposerCommand( - sshConfig: SshConnection, - command: string -): Promise<{ - stdout: string; - stderr: string; - code: number | null; -}> { - const fullCommand = `cd ${sshConfig.remotePath} && composer ${command}`; - return await executeRemoteCommand(sshConfig, fullCommand); -} - -/** - * Get a list of available artisan commands - */ -export async function getArtisanCommandList( - sshConfig: SshConnection -): Promise { - try { - const result = await executeArtisanCommand(sshConfig, 'list --raw'); - - if (result.code !== 0) { - throw new Error( - `Error getting artisan command list: ${result.stderr}` - ); - } - - // Parse the command list - return result.stdout - .split('\n') - .map((line) => line.trim()) - .filter(Boolean); - } catch (error) { - console.error('Error getting artisan command list:', error); - throw error; - } -} - -/** - * Run migrations on the remote server - */ -export async function runMigrations( - sshConfig: SshConnection, - options: { - fresh?: boolean; - seed?: boolean; - force?: boolean; - step?: number; - } = {} -): Promise<{ - stdout: string; - stderr: string; - code: number | null; -}> { - let command = 'migrate'; - - if (options.fresh) { - command = 'migrate:fresh'; - } - - if (options.seed) { - command += ' --seed'; - } - - if (options.force) { - command += ' --force'; - } - - if (options.step && options.step > 0) { - command += ` --step=${options.step}`; - } - - return await executeArtisanCommand(sshConfig, command); -} - -/** - * Get the status of migrations - */ -export async function getMigrationStatus(sshConfig: SshConnection): Promise<{ - stdout: string; - stderr: string; - code: number | null; -}> { - return await executeArtisanCommand(sshConfig, 'migrate:status'); -} -``` - -### 2. Create a Remote Command Execution Component - -Create a new file `src/components/project/RemoteCommandExecutor.vue` for the UI to execute commands: - -```vue - - - -``` - -### 3. Update Project Dashboard for Remote Commands - -Update the project dashboard in `src/views/project/Dashboard.vue` to include the remote command executor: - -```vue - - - -``` - -### 4. Integrate with Database Migration UI - -If you have an existing migration management UI, update it to handle remote migrations. For example, in `src/components/database/MigrationManager.vue`: - -```vue - - - -``` - -## Verification - -- Ensure remote commands execute correctly -- Test running artisan commands on the remote server -- Verify composer commands work properly -- Test handling of command outputs and error states -- Confirm that migrations can be run on the remote server - -## Next Steps - -After completing these tasks, proceed to [Step 9: Install Dependencies](./09-dependencies.md). diff --git a/docs/ssh-implementation/09-dependencies.md b/docs/ssh-implementation/09-dependencies.md deleted file mode 100644 index e63a603..0000000 --- a/docs/ssh-implementation/09-dependencies.md +++ /dev/null @@ -1,156 +0,0 @@ -# Step 9: Install Dependencies - -In this step, we'll install and configure all the necessary dependencies for the SSH functionality in the application. - -## Tasks - -- [ ] Install SSH2 client library -- [ ] Install type definitions for SSH2 -- [ ] Update package.json with new dependencies -- [ ] Configure build tools for native modules - -## Implementation Details - -### 1. Install SSH2 Client Library - -The primary dependency for SSH support is the `ssh2` package, which is a pure JavaScript implementation of the SSH2 protocol. It provides all the functionality needed for SSH connections, tunneling, and file transfers. - -Run the following command to install the package: - -```bash -npm install ssh2 -``` - -### 2. Install Type Definitions for SSH2 - -For proper TypeScript support, we need to install the type definitions for the SSH2 library: - -```bash -npm install @types/ssh2 --save-dev -``` - -### 3. Update package.json - -After installing the dependencies, verify that they're correctly added to your `package.json` file. It should contain the following entries: - -```json -{ - "dependencies": { - // ... existing dependencies - "ssh2": "^1.11.0" - }, - "devDependencies": { - // ... existing dev dependencies - "@types/ssh2": "^1.11.6" - } -} -``` - -The version numbers may be different depending on the current releases of the packages. - -### 4. Configure Electron for Native Modules - -If you're using Electron Builder for packaging your application, you need to ensure it correctly handles the native modules used by SSH2. Add the following to your `electron-builder.yml` or equivalent configuration: - -```yaml -npmRebuild: true -files: - - '!node_modules/ssh2/**/*' - - 'node_modules/ssh2/lib/**/*' -asar: true -asarUnpack: - - 'node_modules/ssh2/**/*' -``` - -Or if you're using the configuration in `package.json`: - -```json -{ - "build": { - "npmRebuild": true, - "files": ["!node_modules/ssh2/**/*", "node_modules/ssh2/lib/**/*"], - "asar": true, - "asarUnpack": ["node_modules/ssh2/**/*"] - } -} -``` - -This ensures that Electron Builder correctly packages the SSH2 module and its dependencies. - -### 5. Install Optional Dependencies for Enhanced Functionality - -For enhanced SSH functionality, you may want to install the following optional dependencies: - -#### 5.1. Install bcrypt for Improved Password Handling - -```bash -npm install bcrypt -npm install @types/bcrypt --save-dev -``` - -This can be used for securely handling passwords when storing SSH credentials. - -#### 5.2. Install node-forge for Key Management - -```bash -npm install node-forge -npm install @types/node-forge --save-dev -``` - -This can be used for handling SSH key formats and conversions. - -### 6. Verify Installation - -After installing all dependencies, verify that everything is working correctly by building your application: - -```bash -npm run build -``` - -If there are any errors related to the SSH2 package or its dependencies, you may need to install additional build tools or resolve conflicts. - -## Troubleshooting Common Issues - -### SSH2 Installation Issues - -If you encounter issues installing the SSH2 package, it might be due to its native dependencies. Try the following: - -1. Ensure you have the necessary build tools installed on your system: - - - On Windows: Install Visual Studio Build Tools with C++ support - - On macOS: Install Xcode Command Line Tools - - On Linux: Install the appropriate build tools (`build-essential` on Ubuntu/Debian) - -2. If you're behind a proxy or have network issues, try: - - ```bash - npm config set registry https://registry.npmjs.org/ - npm install ssh2 --no-proxy - ``` - -3. If you're still having issues, you can try using a pre-built binary: - ```bash - npm install ssh2-prebuilt - ``` - -### Electron Packaging Issues - -If you encounter issues packaging the application with Electron Builder, try these solutions: - -1. Make sure you're using a compatible version of Electron with the SSH2 package -2. Try rebuilding native modules for your Electron version: - ```bash - npx electron-rebuild -f -m ./node_modules/ssh2 - ``` -3. Check if any other dependencies conflict with SSH2 and resolve those conflicts - -## Verification - -- Ensure all dependencies are correctly installed -- Verify that the application builds without errors -- Check that the SSH2 module is correctly packaged with Electron Builder -- Test basic SSH functionality to confirm everything is working - -## Next Steps - -After completing these tasks, proceed to [Step 10: Testing & Verification](./10-testing.md). diff --git a/docs/ssh-implementation/10-testing.md b/docs/ssh-implementation/10-testing.md deleted file mode 100644 index 8655752..0000000 --- a/docs/ssh-implementation/10-testing.md +++ /dev/null @@ -1,295 +0,0 @@ -# Step 10: Testing & Verification - -In this step, we'll perform comprehensive testing and verification of the SSH implementation to ensure all functionality works correctly. - -## Tasks - -- [ ] Set up a test environment -- [ ] Test SSH connection establishment -- [ ] Test remote file operations -- [ ] Test remote command execution -- [ ] Test database operations through SSH tunnel -- [ ] Verify UI components and badges -- [ ] Document any issues and fixes - -## Implementation Details - -### 1. Set Up a Test Environment - -Before testing the SSH functionality, you'll need to set up a suitable test environment: - -#### 1.1. Test SSH Server - -You have several options for setting up a test SSH server: - -**Option 1: Use a remote server you already have access to** - -- Make sure it has PHP and Laravel installed -- Ensure you have SSH access with a valid username/password or key - -**Option 2: Set up a local virtual machine** - -- Install VirtualBox or another virtualization software -- Set up a Linux VM (Ubuntu Server is a good choice) -- Install SSH server, PHP, and set up a Laravel project -- Configure the VM network to allow SSH access from your host machine - -**Option 3: Use Docker** - -- Create a Docker container with SSH, PHP, and Laravel -- Example Dockerfile: - -```dockerfile -FROM php:8.1-fpm - -# Install dependencies -RUN apt-get update && apt-get install -y \ - openssh-server \ - git \ - unzip \ - libzip-dev \ - && docker-php-ext-install zip pdo pdo_mysql - -# Configure SSH -RUN mkdir /var/run/sshd -RUN echo 'root:password' | chpasswd -RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config - -# Install Composer -RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer - -# Set up Laravel project -WORKDIR /var/www -RUN composer create-project laravel/laravel test-project - -EXPOSE 22 80 -CMD ["/usr/sbin/sshd", "-D"] -``` - -Build and run the container: - -```bash -docker build -t ssh-laravel-test . -docker run -d -p 2222:22 -p 8000:80 ssh-laravel-test -``` - -### 2. Test SSH Connection Establishment - -Test the basic SSH connection functionality: - -#### 2.1. Create a new SSH connection - -1. Open the application -2. Go to the connection management screen -3. Click "Add Connection" -4. Select "SSH Remote" as the connection type -5. Fill in the connection details: - - Name: "Test SSH Connection" - - SSH Host: Your test server address - - SSH Port: 22 (or your custom port) - - Username: Your SSH username - - Authentication: Use either password or private key - - Remote Path: Path to your Laravel project - - Remote DB Type: Select MySQL or PostgreSQL - - Remote DB Config: Configure based on your test server -6. Click "Test Connection" to verify -7. Save the connection - -#### 2.2. Verify in Connection List - -1. Check that the connection appears in the list -2. Verify that it has the SSH/Remote badge -3. Check that the connection type is displayed correctly - -### 3. Test Remote File Operations - -Test the remote file browser and file operations: - -#### 3.1. Browse remote files - -1. Open the remote project -2. Navigate to the remote file browser -3. Check that the root directory of the Laravel project is displayed -4. Navigate through directories to verify browsing works -5. Check that file details (size, modification time) are displayed correctly - -#### 3.2. View and edit files - -1. Select a text file (e.g., `.env`) to view -2. Verify that the file content is displayed correctly -3. Try editing the file and saving changes -4. Refresh to confirm changes were saved -5. Try editing different file types (PHP, JavaScript, etc.) - -#### 3.3. Test error handling - -1. Try accessing a non-existent file -2. Try editing a file without write permissions -3. Verify that appropriate error messages are displayed - -### 4. Test Remote Command Execution - -Test the remote command execution functionality: - -#### 4.1. Artisan commands - -1. Navigate to the remote command execution interface -2. Select the "Artisan" tab -3. Try running basic commands: - - `php artisan --version` - - `php artisan list` - - `php artisan route:list` -4. Check that command output is displayed correctly -5. Verify that error messages are displayed for invalid commands - -#### 4.2. Composer commands - -1. Select the "Composer" tab -2. Try running basic commands: - - `composer --version` - - `composer show` - - `composer dump-autoload` -3. Check that output is displayed correctly - -#### 4.3. Custom commands - -1. Select the "Custom Command" tab -2. Try running basic shell commands: - - `ls -la` - - `pwd` - - `whoami` -3. Verify that output is displayed correctly - -### 5. Test Database Operations Through SSH Tunnel - -Test connecting to the remote database through an SSH tunnel: - -#### 5.1. Test database connection - -1. Open the database view for the remote connection -2. Verify that the connection is established -3. Check that the database tables are listed correctly - -#### 5.2. Test queries - -1. Open the SQL executor -2. Run a simple query (e.g., `SELECT 1`) -3. Run a query to list tables -4. Execute a query on one of the tables -5. Verify that results are displayed correctly - -#### 5.3. Test migrations - -1. Navigate to the migration management interface -2. Check the migration status -3. Try running a migration -4. Verify that the migration is executed correctly - -### 6. Verify UI Components and Badges - -Verify that all UI elements related to SSH connections work correctly: - -#### 6.1. Check remote badges - -1. Verify that remote badges are displayed in the project list -2. Check that connection type badges are correct -3. Verify that remote badges appear in all relevant views - -#### 6.2. Check SSH-specific UI elements - -1. Verify that SSH connection details are displayed correctly -2. Check that the remote file browser is only shown for SSH connections -3. Verify that the remote command executor is only shown for SSH connections - -### 7. Test Edge Cases and Error Handling - -Test various edge cases to ensure robust error handling: - -#### 7.1. Connection issues - -1. Test with incorrect SSH credentials -2. Test with an invalid SSH host -3. Test with an unreachable server -4. Verify that appropriate error messages are displayed - -#### 7.2. Permission issues - -1. Test accessing files without read permissions -2. Test editing files without write permissions -3. Test executing commands without execute permissions -4. Verify that appropriate error messages are displayed - -#### 7.3. Resource limitations - -1. Test with large files (>10MB) -2. Test with commands that produce large amounts of output -3. Test with long-running commands -4. Verify that the application handles these cases gracefully - -### 8. Documentation and Fixes - -Document any issues encountered during testing and implement fixes: - -#### 8.1. Document known issues - -Create a document listing any known issues or limitations of the SSH implementation, such as: - -- Maximum file size restrictions -- Command execution timeouts -- Unsupported SSH features - -#### 8.2. Fix issues - -Address any issues identified during testing: - -1. Fix bugs in the code -2. Improve error handling -3. Enhance user feedback for long-running operations - -#### 8.3. Update documentation - -Update any documentation to reflect changes made during testing: - -1. Update README or user guide -2. Update code comments -3. Add troubleshooting tips - -## Final Verification Checklist - -Use this checklist for final verification of the SSH implementation: - -- [ ] SSH connections can be created and saved successfully -- [ ] Connection badges correctly indicate remote connections -- [ ] Remote file browser displays files and directories correctly -- [ ] Files can be viewed and edited remotely -- [ ] Artisan commands execute successfully -- [ ] Composer commands execute successfully -- [ ] Custom shell commands execute successfully -- [ ] Database connections through SSH tunnels work correctly -- [ ] Migrations can be run on the remote server -- [ ] Error handling works as expected -- [ ] UI elements are displayed correctly -- [ ] Performance is acceptable for all operations -- [ ] Edge cases are handled gracefully - -## Next Steps - -After completing testing and verification, the SSH implementation should be ready for production use. Some potential future enhancements to consider: - -1. **Performance optimizations**: - - - Connection pooling for SSH connections - - Caching of remote file information - - Asynchronous command execution for long-running commands - -2. **Feature enhancements**: - - - Multi-server SSH management - - Script storage for frequently used commands - - Scheduled command execution - - File synchronization between local and remote projects - -3. **Security enhancements**: - - Better password/key storage - - Session timeout handling - - Connection activity logging diff --git a/electron/helpers/docker.ts b/electron/helpers/docker.ts index d0822ad..f91cb09 100644 --- a/electron/helpers/docker.ts +++ b/electron/helpers/docker.ts @@ -3,15 +3,6 @@ import fs from 'fs'; import * as zlib from 'node:zlib'; import { RestorationProgressConfig } from '../../src/types/project'; -declare module 'dockerode' { - interface Container { - getExec(id: string): Docker.Exec; - } - interface Exec { - stop(): Promise; - } -} - interface DockerExecStream extends NodeJS.ReadWriteStream { dockerExecId?: string; dockerContainer?: string; @@ -49,42 +40,6 @@ async function isDockerAvailable() { } } -async function isDockerRunning() { - try { - return await isDockerAvailable(); - } catch (error) { - return false; - } -} - -async function getDockerContainers() { - try { - const docker = createDockerClient(); - const containers = await docker.listContainers({ all: false }); - - return containers.map((container) => { - const name = container.Names[0].replace(/^\//, ''); - - let ports = ''; - if (container.Ports && container.Ports.length > 0) { - ports = container.Ports.map((port) => { - if (port.PublicPort && port.PrivatePort) { - return `${port.PublicPort}->${port.PrivatePort}/${port.Type}`; - } else if (port.PrivatePort) { - return `${port.PrivatePort}/${port.Type}`; - } - return ''; - }).join(', '); - } - - return `${name} ${ports}`; - }); - } catch (error) { - console.error('Error listing Docker containers:', error.message); - return []; - } -} - async function detectDockerMysql(port: number) { const result = { dockerAvailable: false, @@ -138,21 +93,6 @@ async function detectDockerMysql(port: number) { } } -async function checkDockerRedis() { - try { - const docker = createDockerClient(); - const containers = await docker.listContainers(); - - return containers.some((container) => { - const name = container.Names[0].replace(/^\//, ''); - return name.toLowerCase().includes('redis'); - }); - } catch (error) { - console.error('Error checking Docker Redis:', error.message); - return false; - } -} - async function executeMysqlFileInContainer( restorationConfig: RestorationProgressConfig, progressCallback: Function, @@ -239,12 +179,12 @@ async function executeMysqlFileInContainer( execStream.dockerExecId = exec.id; execStream.dockerContainer = containerName; - let output = ''; + let _output = ''; execStream.on('data', (chunk) => { if (isCancelled) return; - output += chunk.toString(); + _output += chunk.toString(); }); let totalSize = fs.statSync(sqlFilePath).size; @@ -505,9 +445,6 @@ async function executeMysqlFileInContainer( export { createDockerClient, isDockerAvailable, - isDockerRunning, - getDockerContainers, detectDockerMysql, - checkDockerRedis, executeMysqlFileInContainer }; diff --git a/electron/helpers/mysql.ts b/electron/helpers/mysql.ts index 031dba7..21857e0 100644 --- a/electron/helpers/mysql.ts +++ b/electron/helpers/mysql.ts @@ -1,7 +1,13 @@ -import mysql, { ConnectionOptions, Pool } from 'mysql2/promise'; -import { MysqlConnection } from '../../src/types/mysql-connection'; +import mysql, { + ConnectionOptions, + Pool, + PoolConnection, + RowDataPacket +} from 'mysql2/promise'; +import { createTunnel, closeTunnel } from './ssh'; +import { AppConnection } from '../../src/types/ssh-connection'; -const ERROR_MESSAGES = { +const ERROR_MESSAGES: Record string)> = { ER_ACCESS_DENIED_ERROR: 'Access denied with the provided credentials', ECONNREFUSED: 'Connection refused - check host and port', ER_BAD_DB_ERROR: (db: string) => `Database '${db}' does not exist` @@ -10,47 +16,140 @@ const ERROR_MESSAGES = { const SELECT_TEST_SQL = 'SELECT 1 AS connection_test'; const connectionPools = new Map(); +const sshTunnels = new Map(); -function validateParams({ host, port, user, database }: MysqlConnection) { - if (!host || !port || !user || !database) { +function validateParams(config: AppConnection): void { + if (!config) { + throw new Error('Connection configuration is missing'); + } + + if (config.remote) { + const remoteConfig = config.remote.remoteDbConfig; + + if (!remoteConfig) { + throw new Error('SSH remote database configuration is missing'); + } + + if (!remoteConfig.host) { + throw new Error('SSH remote database host is missing'); + } + + if (!remoteConfig.port) { + throw new Error('SSH remote database port is missing'); + } + + if (!remoteConfig.database) { + throw new Error('SSH remote database name is missing'); + } + + if (!remoteConfig.user) { + throw new Error('SSH remote database user is missing'); + } + + return; + } + + const local = config.localDbConfig; + + if (!local.host || !local.port || !local.user || !local.database) { throw new Error('Missing required connection parameters'); } } -function getConnectionOptions( - config: MysqlConnection, +async function getConnectionOptions( + config: AppConnection, { useConnectionDb = true, targetDatabase = '' } = {} -): ConnectionOptions { +): Promise { + if (config.remote) { + const tunnelKey = `${config.remote.host}:${config.remote.port}:${config.remote.user}`; + + let localPort: number; + + if (sshTunnels.has(tunnelKey)) { + localPort = sshTunnels.get(tunnelKey)!.localPort; + } else { + try { + const tunnel = await createTunnel( + config.remote, + config.remote.remoteDbConfig.host, + config.remote.remoteDbConfig.port + ); + + localPort = tunnel.localPort; + + sshTunnels.set(tunnelKey, { + localPort, + tunnelId: tunnel.tunnelId + }); + } catch (error) { + throw new Error( + `Failed to create SSH tunnel: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + return { + host: 'localhost', + port: localPort, + user: config.remote.remoteDbConfig.user, + password: config.remote.remoteDbConfig.password, + database: useConnectionDb + ? config.remote.remoteDbConfig.database + : targetDatabase, + connectTimeout: 10000 + }; + } + return { - host: config.host, - port: Number(config.port), - user: config.user, - password: config.password || '', - database: useConnectionDb ? config.database : targetDatabase, + host: config.localDbConfig.host, + port: Number(config.localDbConfig.port), + user: config.localDbConfig.user, + password: config.localDbConfig.password || '', + database: useConnectionDb + ? config.localDbConfig.database + : targetDatabase, connectTimeout: 10000 }; } function getPoolKey( - config: MysqlConnection, + config: AppConnection, useConnectionDb: boolean, targetDatabase: string ): string { - return `${config.host}:${config.port}:${config.user}:${useConnectionDb ? config.database : targetDatabase}`; + const hostPart = config.remote ? 'localhost' : config.localDbConfig.host; + const portPart = config.remote + ? sshTunnels.get( + `${config.remote.host}:${config.remote.port}:${config.remote.user}` + )?.localPort + : config.localDbConfig.port; + + // Para conexões remotas, use as informações do remoteDbConfig + const userPart = config.remote + ? config.remote.remoteDbConfig.user + : config.localDbConfig.user; + + const dbPart = useConnectionDb + ? config.remote + ? config.remote.remoteDbConfig.database + : config.localDbConfig.database + : targetDatabase; + + return `${hostPart}:${portPart}:${userPart}:${dbPart}`; } -function getConnectionPool( - config: MysqlConnection, +async function getConnectionPool( + config: AppConnection, { useConnectionDb = true, targetDatabase = '' } = {} -): Pool { +): Promise { const poolKey = getPoolKey(config, useConnectionDb, targetDatabase); if (!connectionPools.has(poolKey)) { const poolConfig = { - ...getConnectionOptions(config, { + ...(await getConnectionOptions(config, { useConnectionDb, targetDatabase - }), + })), connectionLimit: 10, waitForConnections: true, queueLimit: 0 @@ -63,30 +162,38 @@ function getConnectionPool( } async function createConnection( - config: MysqlConnection, + config: AppConnection, { useConnectionDb = true, targetDatabase = '' } = {} -) { +): Promise { validateParams(config); - const pool = getConnectionPool(config, { useConnectionDb, targetDatabase }); + const pool = await getConnectionPool(config, { + useConnectionDb, + targetDatabase + }); return pool.getConnection(); } -async function releaseConnection(connection: any) { +async function releaseConnection(connection: PoolConnection): Promise { if (connection && typeof connection.release === 'function') { connection.release(); } } async function ping( - config: MysqlConnection, + config: AppConnection, options: { useConnectionDb?: boolean; targetDatabase?: string } = {} -) { +): Promise { const conn = await createConnection(config, options); try { - const [rows] = await conn.query(SELECT_TEST_SQL); + // Define a interface para o resultado da consulta de teste + interface ConnectionTestRow extends RowDataPacket { + connection_test: number; + } + + const [rows] = await conn.query(SELECT_TEST_SQL); if (!Array.isArray(rows) || rows.length === 0) { throw new Error('Connection established but query failed'); @@ -96,7 +203,13 @@ async function ping( } } -async function testConnection(config: MysqlConnection) { +interface MysqlError extends Error { + code?: string; +} + +async function testConnection( + config: AppConnection +): Promise<{ success: boolean; message: string }> { try { await ping(config, { useConnectionDb: true }); @@ -104,23 +217,31 @@ async function testConnection(config: MysqlConnection) { } catch (err) { console.error('Error testing MySQL connection:', err); - const custom = ERROR_MESSAGES[err.code]; + const mysqlErr = err as MysqlError; + const errorCode = mysqlErr.code || ''; + + const custom = ERROR_MESSAGES[errorCode]; const message = typeof custom === 'function' - ? custom(config.database) - : custom || err.message; + ? custom(config.localDbConfig?.database || '') + : custom || mysqlErr.message || String(err); return { success: false, message }; } } -function closeAllPools() { - return Promise.all( +async function closeAllPools(): Promise { + await Promise.all( Array.from(connectionPools.values()).map((pool) => pool.end()) ); + + for (const [key, tunnel] of Array.from(sshTunnels.entries())) { + closeTunnel(tunnel.tunnelId); + sshTunnels.delete(key); + } } -async function safeEndConnection(connection?: any) { +async function safeEndConnection(connection?: PoolConnection): Promise { if (connection && typeof connection.end === 'function') { try { await releaseConnection(connection); diff --git a/electron/helpers/ssh.ts b/electron/helpers/ssh.ts new file mode 100644 index 0000000..868c714 --- /dev/null +++ b/electron/helpers/ssh.ts @@ -0,0 +1,594 @@ +import { Client, SFTPWrapper, ConnectConfig } from 'ssh2'; +import * as fs from 'fs'; +import * as net from 'net'; +import * as crypto from 'crypto'; +import { SshConnection } from '../../src/types/ssh-connection'; + +interface SshError extends Error { + code?: string; +} + +interface SftpFile { + filename: string; + longname: string; + attrs: { + size: number; + mtime: number; + atime: number; + uid: number; + gid: number; + mode: number; + [key: string]: number | string | boolean | undefined; + }; +} + +interface FileExplorerItem { + name: string; + type: 'file' | 'directory'; + size: number; + modTime: number; + path: string; +} + +const sshConnections = new Map(); + +const activeTunnels = new Map< + string, + { + server: net.Server; + localPort: number; + remoteHost: string; + remotePort: number; + sshClient: Client; + } +>(); + +function getConnectionKey(config: SshConnection): string { + return `${config.user}@${config.host}:${config.port}:${config.remotePath}`; +} + +function generateTunnelId(): string { + return crypto.randomBytes(16).toString('hex'); +} + +async function createConnection(config: SshConnection): Promise { + return new Promise((resolve, reject) => { + const conn = new Client(); + + const connectConfig: ConnectConfig = { + host: config.host, + port: config.port, + username: config.user, // ssh2 uses username, not user + debug: (message: string) => console.log(`SSH Debug: ${message}`), + readyTimeout: 10000 + }; + + if (config.privateKey) { + try { + console.log(`Reading private key from: ${config.privateKey}`); + const keyData = fs.readFileSync(config.privateKey, 'utf8'); + + if (keyData.includes('BEGIN')) { + connectConfig.privateKey = keyData; + } else { + connectConfig.privateKey = fs.readFileSync( + config.privateKey + ); + } + + if (config.passphrase) { + connectConfig.passphrase = config.passphrase; + } + } catch (error) { + console.error('Error reading private key:', error); + reject( + new Error( + `Failed to read private key: ${ + error instanceof Error + ? error.message + : String(error) + }` + ) + ); + return; + } + } else if (config.password) { + connectConfig.password = config.password; + } else { + reject(new Error('No authentication method provided')); + return; + } + + conn.on('ready', () => { + console.log(`SSH connection established to ${config.host}`); + resolve(conn); + }); + + conn.on('error', (err) => { + console.error(`SSH connection error to ${config.host}:`, err); + reject(err); + }); + + console.log( + `Connecting to SSH server ${config.host}:${config.port} as ${config.user}` + ); + conn.connect(connectConfig); + }); +} + +async function getConnection(config: SshConnection): Promise { + return createConnection(config); +} + +async function getSftpSession(client: Client): Promise { + return new Promise((resolve, reject) => { + client.sftp((err, sftp) => { + if (err) reject(err); + else resolve(sftp); + }); + }); +} + +async function executeCommand( + config: SshConnection, + command: string +): Promise<{ stdout: string; stderr: string; code: number | null }> { + const client = await createConnection(config); + + return new Promise((resolve, reject) => { + client.exec(command, (err, stream) => { + if (err) { + client.end(); + reject(err); + return; + } + + let stdout = ''; + let stderr = ''; + + stream.on('data', (data: Buffer) => { + stdout += data.toString(); + }); + + stream.stderr.on('data', (data: Buffer) => { + stderr += data.toString(); + }); + + stream.on('close', (code: number | null) => { + client.end(); + resolve({ stdout, stderr, code }); + }); + + stream.on('error', (err: Error) => { + client.end(); + reject(err); + }); + }); + }); +} + +async function readRemoteFile( + config: SshConnection, + filePath: string +): Promise { + const client = await createConnection(config); + + try { + const sftp = await getSftpSession(client); + + return new Promise((resolve, reject) => { + sftp.readFile(filePath, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + + client.end(); + }); + }); + } catch (error) { + client.end(); + throw error; + } +} + +async function writeRemoteFile( + config: SshConnection, + filePath: string, + data: Buffer | string +): Promise { + const client = await createConnection(config); + + try { + const sftp = await getSftpSession(client); + + return new Promise((resolve, reject) => { + sftp.writeFile(filePath, data, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + + client.end(); + }); + }); + } catch (error) { + client.end(); + throw error; + } +} + +async function listRemoteFiles( + config: SshConnection, + dirPath: string +): Promise { + const client = await createConnection(config); + + // Safety check to make sure we don't go outside the project directory + if (!dirPath.startsWith(config.remotePath)) { + // If dirPath is outside remotePath, default to remotePath + dirPath = config.remotePath; + } + + try { + console.log('SSH connection established, getting SFTP session'); + const sftp = await getSftpSession(client); + console.log('SFTP session created, attempting to read directory'); + + return new Promise((resolve, reject) => { + sftp.readdir(dirPath, (err, list) => { + const closeAndReturn = (value: any, isError = false) => { + try { + client.end(); + console.log( + `SSH connection closed after listing files in ${dirPath}` + ); + } catch (e) { + console.error('Error closing SSH connection:', e); + } + + if (isError) { + reject(value); + } else { + resolve(value); + } + }; + + if (err) { + console.error(`Error listing files in ${dirPath}:`, err); + + const sshErr = err as SshError; + if (sshErr.code === 'ENOENT') { + closeAndReturn( + new Error(`Directory does not exist: ${dirPath}`), + true + ); + } else if (sshErr.code === 'EACCES') { + closeAndReturn( + new Error( + `Permission denied for directory: ${dirPath}` + ), + true + ); + } else { + closeAndReturn( + new Error(`Failed to list files: ${err.message}`), + true + ); + } + } else { + console.log(`Listed ${list.length} files in ${dirPath}`); + + const fileExplorerItems = list + .filter( + (item) => + item.filename !== '.' && item.filename !== '..' + ) + .map((item) => { + const isDirectory = + (item.attrs.mode & 0o40000) !== 0; + return { + name: item.filename, + type: isDirectory ? 'directory' : 'file', + size: item.attrs.size, + modTime: item.attrs.mtime * 1000, // Convert to JavaScript timestamp + path: `${dirPath}/${item.filename}` + } as FileExplorerItem; + }); + + closeAndReturn(fileExplorerItems); + } + }); + }); + } catch (error) { + console.error('SSH connection error in listRemoteFiles:', error); + client.end(); + throw error; + } +} + +function closeConnection(config: SshConnection): void { + const key = getConnectionKey(config); + + if (sshConnections.has(key)) { + const conn = sshConnections.get(key)!; + conn.end(); + sshConnections.delete(key); + } +} + +function closeAllConnections(): void { + closeAllTunnels(); + + for (const conn of Array.from(sshConnections.values())) { + try { + conn.end(); + } catch (e: unknown) { + console.error( + `Failed to close SSH connection: ${ + e instanceof Error ? e.message : String(e) + }` + ); + } + } + + sshConnections.clear(); +} + +async function testConnection( + config: SshConnection +): Promise<{ success: boolean; message: string }> { + let client: Client | null = null; + + try { + console.log('Testing SSH connection to:', config.host); + + const connectConfig: ConnectConfig = { + host: config.host, + port: config.port, + username: config.user, // ssh2 uses username, not user + debug: (message: string) => console.log(`SSH Debug: ${message}`), + readyTimeout: 10000, + tryKeyboard: true + }; + + if (config.privateKey) { + try { + console.log( + 'Using private key authentication:', + config.privateKey + ); + connectConfig.privateKey = fs.readFileSync(config.privateKey); + + if (config.passphrase) { + connectConfig.passphrase = config.passphrase; + } + } catch (error) { + return { + success: false, + message: `Error reading private key file: ${ + error instanceof Error ? error.message : String(error) + }` + }; + } + } else if (config.password) { + console.log('Using password authentication'); + connectConfig.password = config.password; + } else { + return { + success: false, + message: + 'No authentication method provided. Please provide either a password or a private key.' + }; + } + + client = new Client(); + + return new Promise((resolve) => { + client.on('error', (err) => { + console.error('SSH connection error:', err); + resolve({ + success: false, + message: `Connection error: ${err.message}` + }); + }); + + client.on( + 'keyboard-interactive', + (_name, _instructions, _lang, prompts, finish) => { + console.log('Keyboard interactive auth requested'); + if (config.password && prompts.length > 0) { + finish([config.password]); + } else { + finish([]); + } + } + ); + + client.on('ready', async () => { + console.log( + 'SSH connection established, testing remote path access' + ); + try { + const testResult = await executeCommand( + config, + `cd ${config.remotePath} && ls -la` + ); + + if (testResult.code !== 0) { + resolve({ + success: false, + message: `Error accessing remote path: ${ + testResult.stderr || testResult.stdout + }` + }); + return; + } + + const laravelCheckResult = await executeCommand( + config, + `cd ${config.remotePath} && [ -f artisan ] && echo "Laravel" || echo "Not Laravel"` + ); + + if (laravelCheckResult.stdout.trim() !== 'Laravel') { + resolve({ + success: false, + message: + 'The remote path does not appear to be a Laravel project (no artisan file found)' + }); + return; + } + + resolve({ + success: true, + message: + 'Successfully connected to remote Laravel project' + }); + } catch (innerError) { + resolve({ + success: false, + message: `Connected to SSH server but failed to verify Laravel project: ${ + innerError instanceof Error + ? innerError.message + : String(innerError) + }` + }); + } finally { + client.end(); + } + }); + + setTimeout(() => { + if (client) { + client.end(); + resolve({ + success: false, + message: 'Connection timed out' + }); + } + }, 15000); + + console.log('Attempting to connect to SSH server'); + client.connect(connectConfig); + }); + } catch (error) { + console.error('SSH test connection error:', error); + return { + success: false, + message: + error instanceof Error + ? `SSH connection error: ${error.message}` + : 'Unknown error connecting to SSH server' + }; + } finally { + if (client) { + const clientState = (client as unknown as { _state?: string }) + ._state; + if (clientState === 'authenticated') { + client.end(); + } + } + } +} + +async function createTunnel( + config: SshConnection, + remoteHost: string, + remotePort: number, + localPort: number = 0 +): Promise<{ tunnelId: string; localPort: number }> { + // This is the critical fix - always create a fresh connection for tunneling + // This ensures we don't reuse a connection that might be in a bad state + // from other operations like SFTP + const sshClient = await createConnection(config); + + return new Promise((resolve, reject) => { + const server = net.createServer((socket) => { + sshClient.forwardOut( + '127.0.0.1', + socket.localPort || 0, + remoteHost, + remotePort, + (err, stream) => { + if (err) { + socket.end(); + console.error('SSH tunnel error:', err); + return; + } + + socket.pipe(stream); + stream.pipe(socket); + + stream.on('close', () => { + socket.end(); + }); + + socket.on('close', () => { + stream.end(); + }); + } + ); + }); + + server.on('error', (err) => { + reject(err); + }); + + server.listen(localPort, '127.0.0.1', () => { + const serverAddress = server.address() as net.AddressInfo; + const tunnelId = generateTunnelId(); + + activeTunnels.set(tunnelId, { + server, + localPort: serverAddress.port, + remoteHost, + remotePort, + sshClient + }); + + resolve({ + tunnelId, + localPort: serverAddress.port + }); + }); + }); +} + +function closeTunnel(tunnelId: string): boolean { + if (activeTunnels.has(tunnelId)) { + const tunnel = activeTunnels.get(tunnelId)!; + + tunnel.server.close(); + activeTunnels.delete(tunnelId); + + return true; + } + + return false; +} + +function closeAllTunnels(): void { + for (const tunnel of Array.from(activeTunnels.values())) { + tunnel.server.close(); + } + + activeTunnels.clear(); +} + +export { + createConnection, + getConnection, + getSftpSession, + executeCommand, + readRemoteFile, + writeRemoteFile, + listRemoteFiles, + closeConnection, + closeAllConnections, + testConnection, + createTunnel, + closeTunnel, + closeAllTunnels +}; diff --git a/electron/main/index.ts b/electron/main/index.ts index b445d97..ea07b3c 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, shell } from 'electron'; +import { app, BrowserWindow, shell, ipcMain } from 'electron'; import Store from 'electron-store'; import { fileURLToPath } from 'node:url'; import enhancePath from '../helpers/enhance-path'; @@ -16,7 +16,9 @@ import { registerMonitoringHandlers } from '../modules/monitoring'; import { registerMigrationHandlers } from '../modules/migrations'; import { registerSqlExecutorHandlers } from '../modules/sql-executor'; import { registerUpdaterHandlers, cleanup } from '../modules/updater'; +import { registerSshHandlers } from '../modules/ssh'; import { closeAllPools } from '../helpers/mysql'; +import { closeAllConnections, closeAllTunnels } from '../helpers/ssh'; let handlersRegistered = false; @@ -42,17 +44,21 @@ if (!app.requestSingleInstanceLock()) { process.exit(0); } -const openDevToolsOnInit: boolean = true; +const openDevToolsOnInit: boolean = false; -let win: BrowserWindow | null = null; +let homeWindow: BrowserWindow | null = null; + +const connectionWindows = new Map(); + +const sqlEditorWindows = new Map(); const preload = path.join(__dirname, '../preload/index.mjs'); const indexHtml = path.join(RENDERER_DIST, 'index.html'); -async function createWindow() { +async function createHomeWindow() { enhancePath(); - win = new BrowserWindow({ + homeWindow = new BrowserWindow({ title: 'Larabase', icon: path.join(process.env.VITE_PUBLIC, 'favicon.ico'), width: 1200, @@ -60,7 +66,6 @@ async function createWindow() { minWidth: 1200, minHeight: 500, resizable: true, - alwaysOnTop: false, center: true, titleBarStyle: 'hiddenInset', webPreferences: { @@ -70,35 +75,255 @@ async function createWindow() { } }); - registerHandlers(win); + registerHandlers(homeWindow); if (VITE_DEV_SERVER_URL) { - await win.loadURL(VITE_DEV_SERVER_URL); + await homeWindow.loadURL(VITE_DEV_SERVER_URL); if (openDevToolsOnInit) { - win.webContents.openDevTools(); + homeWindow.webContents.openDevTools(); } } else { - await win.loadFile(indexHtml); + await homeWindow.loadFile(indexHtml); } - win.webContents.on('did-finish-load', () => { - win?.webContents.send( + homeWindow.webContents.on('did-finish-load', () => { + homeWindow?.webContents.send( 'main-process-message', new Date().toLocaleString() ); }); - win.webContents.setWindowOpenHandler(({ url }) => { + homeWindow.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith('https:')) shell.openExternal(url); + return { action: 'deny' }; + }); + + homeWindow.on('closed', () => { + homeWindow = null; + }); +} + +async function createConnectionWindow(connectionId: string, isRemote: boolean) { + for (const [id, window] of Array.from(connectionWindows.entries())) { + closeAllSqlEditorsForConnection(id); + + window.close(); + connectionWindows.delete(id); + } + + const connectionWindow = new BrowserWindow({ + title: `Larabase - Connection ${connectionId}`, + icon: path.join(process.env.VITE_PUBLIC, 'favicon.ico'), + width: 1280, + height: 900, + minWidth: 1200, + minHeight: 600, + resizable: true, + center: true, + titleBarStyle: 'hiddenInset', + webPreferences: { + preload, + nodeIntegration: true, + contextIsolation: true + }, + show: false + }); + + registerHandlers(connectionWindow); + + let url = VITE_DEV_SERVER_URL + ? `${VITE_DEV_SERVER_URL}#/database/${connectionId}/${isRemote}` + : `file://${indexHtml}#/database/${connectionId}/${isRemote}`; + + await connectionWindow.loadURL(url); + + connectionWindow.show(); + + if (openDevToolsOnInit) { + connectionWindow.webContents.openDevTools(); + } + + connectionWindow.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('https:')) shell.openExternal(url); return { action: 'deny' }; }); + + connectionWindow.on('closed', () => { + closeAllSqlEditorsForConnection(connectionId); + + connectionWindows.delete(connectionId); + + if (!homeWindow) { + createHomeWindow(); + } else { + homeWindow.show(); + } + }); + + connectionWindows.set(connectionId, connectionWindow); + + connectionWindow.on('close', (_e) => { + if ( + process.platform !== 'darwin' && + !homeWindow && + connectionWindows.size === 1 + ) { + app.quit(); + } + }); + + return connectionWindow; } -app.whenReady().then(createWindow); +async function createSqlEditorWindow(connectionId: string, isRemote: boolean) { + const sqlEditorWindow = new BrowserWindow({ + title: `Larabase - SQL Editor ${connectionId}`, + icon: path.join(process.env.VITE_PUBLIC, 'favicon.ico'), + width: 1280, + height: 900, + minWidth: 1200, + minHeight: 600, + resizable: true, + center: true, + titleBarStyle: 'hiddenInset', + webPreferences: { + preload, + nodeIntegration: true, + contextIsolation: true + }, + show: false + }); + + registerHandlers(sqlEditorWindow); + + let url = VITE_DEV_SERVER_URL + ? `${VITE_DEV_SERVER_URL}#/sql-editor/${connectionId}/${isRemote}` + : `file://${indexHtml}#/sql-editor/${connectionId}/${isRemote}`; + + await sqlEditorWindow.loadURL(url); + + sqlEditorWindow.show(); + + if (openDevToolsOnInit) { + sqlEditorWindow.webContents.openDevTools(); + } + + sqlEditorWindow.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith('https:')) shell.openExternal(url); + return { action: 'deny' }; + }); + + if (!sqlEditorWindows.has(connectionId)) { + sqlEditorWindows.set(connectionId, []); + } + + sqlEditorWindows.get(connectionId)?.push(sqlEditorWindow); + + sqlEditorWindow.on('closed', () => { + const windows = sqlEditorWindows.get(connectionId) || []; + const index = windows.indexOf(sqlEditorWindow); + if (index !== -1) { + windows.splice(index, 1); + } + + if (windows.length === 0) { + sqlEditorWindows.delete(connectionId); + } + }); + + return sqlEditorWindow; +} + +function updatePendingMigrationsBadge(count: number) { + if (process.platform === 'darwin') { + const displayCount = + count > 0 ? (count >= 100 ? '99' : count.toString()) : ''; + app.dock.setBadge(displayCount); + } else if (process.platform === 'win32' || process.platform === 'linux') { + const displayCount = count > 0 ? (count >= 100 ? 99 : count) : 0; + app.setBadgeCount(displayCount); + } +} + +function registerWindowHandlers() { + ipcMain.handle('update-migrations-badge', (_, count) => { + updatePendingMigrationsBadge(count); + return true; + }); + + ipcMain.handle( + 'open-connection-window', + async (_, connectionId, isRemote) => { + const window = await createConnectionWindow(connectionId, isRemote); + + if (homeWindow) { + homeWindow.hide(); + } + + return !!window; + } + ); + + ipcMain.handle('close-connection-window', (_, connectionId) => { + const window = connectionWindows.get(connectionId); + if (window) { + window.close(); + return true; + } + return false; + }); + + ipcMain.handle( + 'open-sql-editor-window', + async (_, connectionId, isRemote) => { + const window = await createSqlEditorWindow(connectionId, isRemote); + return !!window; + } + ); + + ipcMain.handle('show-home-window', async () => { + for (const [id, window] of Array.from(connectionWindows.entries())) { + closeAllSqlEditorsForConnection(id); + + if (!window.isDestroyed()) { + window.close(); + } + connectionWindows.delete(id); + } + + if (!homeWindow) { + await createHomeWindow(); + } else { + if (homeWindow.isMinimized()) { + homeWindow.restore(); + } + homeWindow.show(); + homeWindow.focus(); + } + return true; + }); + + ipcMain.handle('get-window-id', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win === homeWindow) return 'home'; + + for (const [id, window] of Array.from(connectionWindows.entries())) { + if (window === win) return id; + } + + return null; + }); +} + +app.whenReady().then(async () => { + await createHomeWindow(); + registerWindowHandlers(); +}); app.on('window-all-closed', () => { - win = null; + homeWindow = null; + connectionWindows.clear(); cleanup(); @@ -108,23 +333,36 @@ app.on('window-all-closed', () => { console.error('Error closing all pools:', err); }) .finally(() => { - if (process.platform === 'darwin') app.quit(); + closeAllConnections(); + closeAllTunnels(); + + if (process.platform !== 'darwin') app.quit(); }); }); app.on('second-instance', () => { - if (win) { - if (win.isMinimized()) win.restore(); - win.focus(); + if (homeWindow) { + if (homeWindow.isMinimized()) homeWindow.restore(); + homeWindow.focus(); + } else if (connectionWindows.size > 0) { + const firstWindow = connectionWindows.values().next().value; + if (firstWindow) { + if (firstWindow.isMinimized()) firstWindow.restore(); + firstWindow.focus(); + } } }); app.on('activate', async () => { - const allWindows = BrowserWindow.getAllWindows(); - if (allWindows.length) { - allWindows[0].focus(); + if (homeWindow) { + homeWindow.focus(); + } else if (connectionWindows.size > 0) { + const firstWindow = connectionWindows.values().next().value; + if (firstWindow) { + firstWindow.focus(); + } } else { - await createWindow(); + await createHomeWindow(); } }); @@ -143,6 +381,21 @@ function registerHandlers(win: BrowserWindow) { registerMigrationHandlers(); registerSqlExecutorHandlers(); registerUpdaterHandlers(win); + registerSshHandlers(); handlersRegistered = true; } + +function closeAllSqlEditorsForConnection(connectionId: string) { + const editorWindows = sqlEditorWindows.get(connectionId) || []; + + const windowsToClose = [...editorWindows]; + + for (const window of windowsToClose) { + if (window && !window.isDestroyed()) { + window.close(); + } + } + + sqlEditorWindows.delete(connectionId); +} diff --git a/electron/modules/database-restore.ts b/electron/modules/database-restore.ts index c05f0cf..3dc42f6 100644 --- a/electron/modules/database-restore.ts +++ b/electron/modules/database-restore.ts @@ -20,6 +20,10 @@ import { createDockerClient } from '../helpers/docker'; import { exec } from 'child_process'; +import { toRaw } from 'vue'; +import { AppConnection } from '../../src/types/ssh-connection'; +import { MysqlConnection } from '../../src/types/mysql-connection'; +import { PoolConnection } from 'mysql2/promise'; let activeRestoreProcess = null; @@ -54,11 +58,12 @@ function buildBaseCommand( return `set -o pipefail && ${command}`; } -function buildCredentialFlags({ user, password, host, port }) { - let flags = ` -u${user || 'root'}`; - if (password) flags += ` -p${password}`; - if (host && host !== 'localhost') flags += ` -h${host}`; - if (port) flags += ` -P${port}`; +function buildCredentialFlags(mysqlConnection: MysqlConnection) { + let flags = ` -u${mysqlConnection.user || 'root'}`; + if (mysqlConnection.password) flags += ` -p${mysqlConnection.password}`; + if (mysqlConnection.host && mysqlConnection.host !== 'localhost') + flags += ` -h${mysqlConnection.host}`; + if (mysqlConnection.port) flags += ` -P${mysqlConnection.port}`; return flags; } @@ -83,10 +88,10 @@ function getFileSizeOrThrow(filePath: string) { function ensureConfig(projectConnection: ProjectConnection, type: string) { if ( !projectConnection || - !projectConnection.db_config.host || - !projectConnection.db_config.port || - !projectConnection.db_config.user || - !projectConnection.db_config.database + !projectConnection.dbConfig.host || + !projectConnection.dbConfig.port || + !projectConnection.dbConfig.user || + !projectConnection.dbConfig.database ) { throw new Error( `Missing connection configuration for ${type} restore command` @@ -95,17 +100,21 @@ function ensureConfig(projectConnection: ProjectConnection, type: string) { } async function validateDatabaseHasContent(restorationObject: RestoreConfig) { - let dbConnection: any; + let dbConnection: PoolConnection; try { try { - dbConnection = await createConnection( - restorationObject.project.db_config, - { - useConnectionDb: false, - targetDatabase: restorationObject.targetDatabase - } - ); + const project = restorationObject.project; + + const AppConnection = { + localDbConfig: toRaw(project.dbConfig), + remote: toRaw(project.sshConfig) + } as AppConnection; + + dbConnection = await createConnection(AppConnection, { + useConnectionDb: false, + targetDatabase: restorationObject.targetDatabase + }); const [rows] = await dbConnection.query(`SHOW TABLES`); @@ -142,7 +151,7 @@ function buildLocalRestoreCommand(restorationObject: RestoreConfig) { let command = buildBaseCommand(filePath, sedFilters, useGunzip, true); command += ' | mysql'; - command += buildCredentialFlags(restorationObject.project.db_config); + command += buildCredentialFlags(restorationObject.project.dbConfig); command += ' --binary-mode=1 --force'; command += ` --init-command="DROP DATABASE IF EXISTS \\\`${targetDatabase}\\\`; CREATE DATABASE \\\`${targetDatabase}\\\`; USE \\\`${targetDatabase}\\\`;"`; command += ` --database=\`${targetDatabase}\``; @@ -152,7 +161,7 @@ function buildLocalRestoreCommand(restorationObject: RestoreConfig) { return { command: command, container: null, - connection: restorationObject.project.db_config, + connection: restorationObject.project.dbConfig, sqlFilePath: restorationObject.filePath, ignoredTables: restorationObject.ignoredTables || [], useDockerApi: false, @@ -177,7 +186,7 @@ function buildDockerRestoreCommand(restorationObject: RestoreConfig) { return { command: null, container: restorationObject.project.dockerInfo.dockerContainerName, - connection: restorationObject.project.db_config, + connection: restorationObject.project.dbConfig, sqlFilePath: restorationObject.filePath, ignoredTables: restorationObject.ignoredTables || [], useDockerApi: true, @@ -214,7 +223,12 @@ async function restoreDatabase( sendProgress('starting', 0, 'Starting database restoration process'); try { - await testConnection(restorationObject.project.db_config); + const AppConnection = { + localDbConfig: toRaw(restorationObject.project.dbConfig), + remote: toRaw(restorationObject.project.sshConfig) + } as AppConnection; + + await testConnection(AppConnection); sendProgress('validating', 10, 'Database connection validated'); } catch (err) { @@ -698,7 +712,7 @@ async function extractTables(filePath: string, isGzipped: boolean) { } tableStats.set(currentTable, stats); - } catch (e) { + } catch (e: unknown) { const stats = tableStats.get(currentTable) || { estimatedRows: 0 }; @@ -711,6 +725,11 @@ async function extractTables(filePath: string, isGzipped: boolean) { } tableStats.set(currentTable, stats); + + console.error( + `Error processing insert for table ${currentTable}:`, + e + ); } currentTable = null; @@ -879,7 +898,7 @@ function registerDatabaseRestoreHandlers(mainWindow: BrowserWindow) { try { if ( !restorationConfig || - !restorationConfig.project.db_config || + !restorationConfig.project.dbConfig || !restorationConfig.filePath ) { return { @@ -898,7 +917,7 @@ function registerDatabaseRestoreHandlers(mainWindow: BrowserWindow) { const targetDatabase = restorationConfig.targetDatabase || - restorationConfig.project.db_config.database; + restorationConfig.project.dbConfig.database; if (!targetDatabase) { return { @@ -928,7 +947,7 @@ function registerDatabaseRestoreHandlers(mainWindow: BrowserWindow) { console.log(`Restoring database: ${targetDatabase}`); console.log( - `Original connection database: ${project.db_config.database}` + `Original connection database: ${project.dbConfig.database}` ); console.log(`Docker mode: ${useDocker ? 'Yes' : 'No'}`); console.log(`Gzipped file: ${isGzipped ? 'Yes' : 'No'}`); diff --git a/electron/modules/migrations.ts b/electron/modules/migrations.ts index 294092d..6c9d759 100644 --- a/electron/modules/migrations.ts +++ b/electron/modules/migrations.ts @@ -4,6 +4,7 @@ import path from 'path'; import { spawn } from 'child_process'; import { RowDataPacket } from 'mysql2'; import { createConnection, safeEndConnection } from '../helpers/mysql'; +import { PoolConnection } from 'mysql2/promise'; interface MigrationRow extends RowDataPacket { id: number; @@ -23,7 +24,7 @@ function registerMigrationHandlers() { }; } - if (!config.db_config) { + if (!config.appConnection) { return { success: false, message: 'Database configuration is required', @@ -144,10 +145,12 @@ function registerMigrationHandlers() { } if (migrationsHistory.length === 0) { - let connection; + let connection: PoolConnection; + try { - connection = await createConnection(config.db_config); + connection = await createConnection(config.appConnection); + // noinspection SqlResolve const [rows] = await connection.query( 'SELECT * FROM migrations ORDER BY batch DESC, id DESC' ); diff --git a/electron/modules/monitoring.ts b/electron/modules/monitoring.ts index 08422e6..72becc8 100644 --- a/electron/modules/monitoring.ts +++ b/electron/modules/monitoring.ts @@ -1,7 +1,7 @@ import { BrowserWindow, ipcMain } from 'electron'; -import { MysqlConnection } from '../../src/types/mysql-connection'; import { createConnection, safeEndConnection } from '../helpers/mysql'; import { RowDataPacket, PoolConnection } from 'mysql2/promise'; +import { AppConnection } from '../../src/types/ssh-connection'; interface ColumnRow extends RowDataPacket { column_name: string; @@ -13,6 +13,11 @@ interface TableRow extends RowDataPacket { TABLE_NAME: string; } +interface TriggerRow extends RowDataPacket { + trigger_name?: string; + TRIGGER_NAME?: string; +} + interface ActivityLogRow extends RowDataPacket { id: number; action_type: string; @@ -33,6 +38,7 @@ const state = { monitoredDatabases: new Map() }; +// noinspection SqlResolve const SQL = { CREATE_ACTIVITY_LOG: ` CREATE TABLE IF NOT EXISTS ${ACTIVITY_LOG_TABLE} ( @@ -94,10 +100,11 @@ const SQL = { LIMIT ? `, GET_NEW_ACTIVITY: ` - SELECT * FROM ${ACTIVITY_LOG_TABLE} - WHERE id > ? - ORDER BY id ASC - `, + SELECT * + FROM ${ACTIVITY_LOG_TABLE} + WHERE id > ? + ORDER BY id + `, TRUNCATE_ACTIVITY_LOG: `TRUNCATE TABLE ${ACTIVITY_LOG_TABLE}`, CHECK_TABLE_EXISTS: ` SELECT 1 @@ -121,8 +128,11 @@ async function dropTableTriggers( for (const trigger of triggers) { try { await connection.query(`DROP TRIGGER IF EXISTS ${trigger}`); - } catch (error) { - console.error(`Error dropping trigger ${trigger}`); + } catch (error: unknown) { + console.error( + `Error dropping trigger ${trigger}`, + error instanceof Error ? error.message : '' + ); } } } @@ -138,7 +148,7 @@ async function findAndDropAllTriggers( ); if (triggersResult && Array.isArray(triggersResult)) { - for (const row of triggersResult as any[]) { + for (const row of triggersResult as TriggerRow[]) { const triggerName = row.trigger_name || row.TRIGGER_NAME; await connection.query(`DROP TRIGGER IF EXISTS ${triggerName}`); } @@ -151,21 +161,27 @@ async function findAndDropAllTriggers( ); if (refTriggers && Array.isArray(refTriggers)) { - for (const row of refTriggers as any[]) { + for (const row of refTriggers as TriggerRow[]) { const triggerName = row.TRIGGER_NAME || row.trigger_name; await connection.query( `DROP TRIGGER IF EXISTS ${triggerName}` ); } } - } catch (error) { + } catch (error: unknown) { console.error( - `Error dropping triggers referencing ${ACTIVITY_LOG_TABLE}` + `Error dropping triggers referencing ${ACTIVITY_LOG_TABLE}`, + error instanceof Error ? error.message : '' ); } return true; - } catch (error) { + } catch (error: unknown) { + console.error( + `Error dropping triggers for ${database}`, + error instanceof Error ? error.message : '' + ); + return false; } } @@ -195,8 +211,11 @@ async function createTableTriggers( const firstColumn = firstColumnResult[0] as ColumnRow; idColumn = firstColumn.column_name || firstColumn.COLUMN_NAME; } - } catch (error) { - console.error(`Error getting first column for ${tableName}`); + } catch (error: unknown) { + console.error( + `Error getting first column for ${tableName}`, + error instanceof Error ? error.message : '' + ); } } @@ -293,8 +312,11 @@ async function createTableTriggers( } return true; - } catch (error) { - console.error(`Error creating triggers for ${tableName}`); + } catch (error: unknown) { + console.error( + `Error creating triggers for ${tableName}`, + error instanceof Error ? error.message : '' + ); return false; } } @@ -303,7 +325,12 @@ async function dropMonitoringTable(connection: PoolConnection) { try { await connection.query(SQL.DROP_ACTIVITY_LOG); return true; - } catch (error) { + } catch (error: unknown) { + console.error( + 'Error dropping monitoring table', + error instanceof Error ? error.message : '' + ); + return false; } } @@ -312,7 +339,12 @@ async function clearActivityLog(connection: PoolConnection) { try { await connection.query(SQL.TRUNCATE_ACTIVITY_LOG); return true; - } catch (error) { + } catch (error: unknown) { + console.error( + 'Error clearing activity log', + error instanceof Error ? error.message : '' + ); + return false; } } @@ -328,6 +360,14 @@ function startPolling( const interval = setInterval(async () => { try { + if (mainWindow.isDestroyed()) { + console.log( + `Window for connection ${connectionId} is destroyed, stopping polling` + ); + stopPolling(connectionId); + return; + } + const [tableExists] = await connection.query( SQL.CHECK_TABLE_EXISTS, [connection.config.database, ACTIVITY_LOG_TABLE] @@ -360,16 +400,33 @@ function startPolling( ].id ); - for (const activity of newActivities as ActivityLogRow[]) { - mainWindow.webContents.send( - `db-operation-${connectionId}`, - activity - ); + if (!mainWindow.isDestroyed()) { + for (const activity of newActivities as ActivityLogRow[]) { + mainWindow.webContents.send( + `db-operation-${connectionId}`, + activity + ); + } } } - } catch (error) { - console.error(`Error polling for new activities: ${error}`); + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error(`Error polling for new activities: ${errorMessage}`); + + if ( + errorMessage.includes('destroyed') || + errorMessage.includes('invalid') + ) { + console.log( + `Reference to destroyed object detected for connection ${connectionId}, stopping polling` + ); + stopPolling(connectionId); + return; + } + state.lastSeenIds.set(connectionId, 0); + stopPolling(connectionId); } }, 2000); @@ -391,22 +448,37 @@ async function cleanupMonitoring(connectionId: string) { const connection = state.connections.get(connectionId); const database = state.monitoredDatabases.get(connectionId); + stopPolling(connectionId); + if (database) { - await findAndDropAllTriggers(connection, database); + try { + await findAndDropAllTriggers(connection, database); - const tables = state.monitoredTables.get(database) || []; - for (const tableName of tables) { - await dropTableTriggers(connection, tableName); - } - state.monitoredTables.delete(database); + const tables = state.monitoredTables.get(database) || []; + for (const tableName of tables) { + await dropTableTriggers(connection, tableName); + } + state.monitoredTables.delete(database); - await dropMonitoringTable(connection); + await dropMonitoringTable(connection); + } catch (err) { + console.error( + 'Error cleaning up tables/triggers:', + err instanceof Error ? err.message : String(err) + ); + } } - await safeEndConnection(connection); + try { + await safeEndConnection(connection); + } catch (err) { + console.error( + 'Error ending connection:', + err instanceof Error ? err.message : String(err) + ); + } state.connections.delete(connectionId); - stopPolling(connectionId); state.monitoredDatabases.delete(connectionId); const windowEntries = Array.from( @@ -419,7 +491,16 @@ async function cleanupMonitoring(connectionId: string) { } return true; - } catch (error) { + } catch (error: unknown) { + console.error( + `Error cleaning up monitoring for ${connectionId}`, + error instanceof Error ? error.message : String(error) + ); + + state.connections.delete(connectionId); + stopPolling(connectionId); + state.monitoredDatabases.delete(connectionId); + return false; } } @@ -444,43 +525,42 @@ function registerConnectionWithWindow(windowId: number, connectionId: string) { } export function registerMonitoringHandlers(mainWindow: BrowserWindow) { - const windowId = mainWindow.id; - - mainWindow.on('closed', async () => { - await closeAllWindowConnections(windowId); - }); - - mainWindow.webContents.on('did-start-navigation', async (_, url) => { - const currentURL = mainWindow.webContents.getURL(); - if (url === currentURL) { - await closeAllWindowConnections(windowId); - } - }); - ipcMain.handle( 'start-live-db-updates', async ( - _, + event, config: { connectionId: string; - dbConnection: MysqlConnection; + appConnection: AppConnection; clearHistory: boolean; } ) => { - const { connectionId, dbConnection, clearHistory = false } = config; + const { + connectionId, + appConnection, + clearHistory = false + } = config; + + const senderWindow = BrowserWindow.fromWebContents(event.sender); + if (!senderWindow || senderWindow.isDestroyed()) { + return { + success: false, + message: 'Window is invalid or destroyed' + }; + } if (state.connections.has(connectionId)) { await cleanupMonitoring(connectionId); } try { - const connection = await createConnection(dbConnection); + const connection = await createConnection(appConnection); state.connections.set(connectionId, connection); state.monitoredDatabases.set( connectionId, - dbConnection.database + appConnection.localDbConfig.database ); - registerConnectionWithWindow(mainWindow.id, connectionId); + registerConnectionWithWindow(senderWindow.id, connectionId); await connection.query(SQL.CREATE_ACTIVITY_LOG); if (clearHistory) { @@ -488,7 +568,7 @@ export function registerMonitoringHandlers(mainWindow: BrowserWindow) { } const [tablesResult] = await connection.query(SQL.GET_TABLES, [ - dbConnection.database, + appConnection.localDbConfig.database, ACTIVITY_LOG_TABLE ]); @@ -497,7 +577,7 @@ export function registerMonitoringHandlers(mainWindow: BrowserWindow) { const tableName = table.table_name || table.TABLE_NAME; const [primaryKeyResult] = await connection.query( SQL.CHECK_PRIMARY_KEY, - [dbConnection.database, tableName] + [appConnection.localDbConfig.database, tableName] ); const primaryKeyColumn = @@ -511,7 +591,7 @@ export function registerMonitoringHandlers(mainWindow: BrowserWindow) { await createTableTriggers( connection, - dbConnection.database, + appConnection.localDbConfig.database, tableName, primaryKeyColumn ); @@ -521,7 +601,10 @@ export function registerMonitoringHandlers(mainWindow: BrowserWindow) { try { const [tableExists] = await connection.query( SQL.CHECK_TABLE_EXISTS, - [dbConnection.database, ACTIVITY_LOG_TABLE] + [ + appConnection.localDbConfig.database, + ACTIVITY_LOG_TABLE + ] ); if (Array.isArray(tableExists) && tableExists.length > 0) { @@ -534,11 +617,13 @@ export function registerMonitoringHandlers(mainWindow: BrowserWindow) { Array.isArray(recentActivities) && recentActivities.length > 0 ) { - for (const activity of recentActivities as ActivityLogRow[]) { - mainWindow.webContents.send( - `db-operation-${connectionId}`, - activity - ); + if (!senderWindow.isDestroyed()) { + for (const activity of recentActivities as ActivityLogRow[]) { + senderWindow.webContents.send( + `db-operation-${connectionId}`, + activity + ); + } } state.lastSeenIds.set( connectionId, @@ -550,19 +635,27 @@ export function registerMonitoringHandlers(mainWindow: BrowserWindow) { } else { state.lastSeenIds.set(connectionId, 0); } - } catch (error) { + } catch (error: unknown) { + console.error( + `Error checking if ${ACTIVITY_LOG_TABLE} exists`, + error instanceof Error ? error.message : String(error) + ); + state.lastSeenIds.set(connectionId, 0); } - startPolling(connectionId, connection, mainWindow); + startPolling(connectionId, connection, senderWindow); return { success: true, message: 'Monitoring started successfully' }; - } catch (error: any) { + } catch (error: unknown) { return { success: false, - message: error.message || 'Failed to start monitoring' + message: + error instanceof Error + ? error.message + : 'Failed to start monitoring' }; } } @@ -572,15 +665,26 @@ export function registerMonitoringHandlers(mainWindow: BrowserWindow) { try { await cleanupMonitoring(connectionId); return { success: true, message: 'Monitoring stopped' }; - } catch (error: any) { + } catch (error: unknown) { return { success: false, - message: error.message || 'Failed to stop monitoring' + message: + error instanceof Error + ? error.message + : 'Failed to stop monitoring' }; } }); - ipcMain.handle('clear-db-history', async (_, connectionId: string) => { + ipcMain.handle('clear-db-history', async (event, connectionId: string) => { + const senderWindow = BrowserWindow.fromWebContents(event.sender); + if (!senderWindow || senderWindow.isDestroyed()) { + return { + success: false, + message: 'Window is invalid or destroyed' + }; + } + try { if (!state.connections.has(connectionId)) { return { @@ -593,7 +697,12 @@ export function registerMonitoringHandlers(mainWindow: BrowserWindow) { try { await connection.query('SELECT 1'); - } catch (pingError) { + } catch (error: unknown) { + console.error( + `Error checking connection status for ${connectionId}`, + error instanceof Error ? error.message : String(error) + ); + state.connections.delete(connectionId); return { success: false, @@ -606,9 +715,13 @@ export function registerMonitoringHandlers(mainWindow: BrowserWindow) { const success = await clearActivityLog(connection); if (success) { state.lastSeenIds.set(connectionId, 0); - mainWindow.webContents.send( - `db-operation-clear-${connectionId}` - ); + + if (!senderWindow.isDestroyed()) { + senderWindow.webContents.send( + `db-operation-clear-${connectionId}` + ); + } + return { success: true, message: 'Activity history cleared' }; } else { return { @@ -616,10 +729,13 @@ export function registerMonitoringHandlers(mainWindow: BrowserWindow) { message: 'Failed to clear activity history' }; } - } catch (error: any) { + } catch (error: unknown) { return { success: false, - message: error.message || 'Failed to clear history' + message: + error instanceof Error + ? error.message + : 'Failed to clear activity history' }; } }); diff --git a/electron/modules/mysql.ts b/electron/modules/mysql.ts index f97bfd9..8e1f521 100644 --- a/electron/modules/mysql.ts +++ b/electron/modules/mysql.ts @@ -4,32 +4,26 @@ import { createConnection, safeEndConnection } from '../helpers/mysql'; -import { MysqlConnection } from '../../src/types/mysql-connection'; +import { AppConnection } from '../../src/types/ssh-connection'; +import { PoolConnection, RowDataPacket } from 'mysql2/promise'; + +interface DatabaseRow extends RowDataPacket { + Database?: string; + database?: string; +} function registerMysqlHandlers() { ipcMain.handle( 'test-mysql-connection', - async (_, config: MysqlConnection) => { + async (_, config: AppConnection) => { return await testConnection(config); } ); ipcMain.handle( 'create-database', - async (_, config: MysqlConnection, databaseName: string) => { - if ( - !config.host || - !config.port || - !config.user || - !config.database - ) { - return { - success: false, - message: 'Missing connection parameters' - }; - } - - let connection: any; + async (_, config: AppConnection, databaseName: string) => { + let connection: PoolConnection; try { connection = await createConnection(config); @@ -42,7 +36,7 @@ function registerMysqlHandlers() { return { success: true, - message: `Database ${config.database} created successfully` + message: `Database ${config.localDbConfig} created successfully` }; } catch (err) { console.error('Error creating database:', err); @@ -52,7 +46,7 @@ function registerMysqlHandlers() { } else if (err.code === 'ECONNREFUSED') { msg = 'Connection refused - check host and port'; } else if (err.code === 'ER_DB_CREATE_EXISTS') { - msg = `Database ${config.database} already exists`; + msg = `Database already exists`; } return { success: false, @@ -64,26 +58,16 @@ function registerMysqlHandlers() { } ); - ipcMain.handle('list-databases', async (_, config: MysqlConnection) => { - if (!config.host || !config.port || !config.user) { - return { - success: false, - message: 'Missing connection parameters', - databases: [] - }; - } - - let connection: any; + ipcMain.handle('list-databases', async (_, config: AppConnection) => { + let connection: PoolConnection; try { connection = await createConnection(config); - const [rows] = await connection.query('SHOW DATABASES'); + const [rows] = + await connection.query('SHOW DATABASES'); const databases = rows - .map( - (r: { Database?: string; database?: string }) => - r.Database || r.database - ) + .map((r: DatabaseRow) => r.Database || r.database) .filter( (db: string) => ![ @@ -114,20 +98,8 @@ function registerMysqlHandlers() { ipcMain.handle( 'drop-database', - async (_, config: MysqlConnection, databaseName: string) => { - if ( - !config.host || - !config.port || - !config.user || - !config.database - ) { - return { - success: false, - message: 'Missing connection parameters' - }; - } - - let connection: any; + async (_, config: AppConnection, databaseName: string) => { + let connection: PoolConnection; try { connection = await createConnection(config); @@ -148,7 +120,7 @@ function registerMysqlHandlers() { } else if (err.code === 'ECONNREFUSED') { msg = 'Connection refused - check host and port'; } else if (err.code === 'ER_DB_DROP_EXISTS') { - msg = `Database ${config.database} does not exist`; + msg = `Database ${config.localDbConfig.database || config.remote.remoteDbConfig.database} does not exist`; } return { success: false, diff --git a/electron/modules/projects.ts b/electron/modules/projects.ts index 75c2535..ab54035 100644 --- a/electron/modules/projects.ts +++ b/electron/modules/projects.ts @@ -31,6 +31,15 @@ function registerProjectHandlers(mainWindow: Electron.BrowserWindow) { } }); + ipcMain.handle('select-file', async (_, options) => { + try { + return await dialog.showOpenDialog(mainWindow, options); + } catch (error) { + console.error('Error selecting file:', error); + throw error; + } + }); + ipcMain.handle('validate-laravel-project', async (_, projectPath) => { try { const hasEnv = fs.existsSync(path.join(projectPath, '.env')); diff --git a/electron/modules/sql-executor.ts b/electron/modules/sql-executor.ts index 4e7da08..ac3b91f 100644 --- a/electron/modules/sql-executor.ts +++ b/electron/modules/sql-executor.ts @@ -1,10 +1,10 @@ import { ipcMain } from 'electron'; import { createConnection, releaseConnection } from '../helpers/mysql'; -import { MysqlConnection } from '../../src/types/mysql-connection'; +import { AppConnection } from '../../src/types/ssh-connection'; async function executeSqlQueryHandler( _: any, - config: MysqlConnection, + config: AppConnection, query: string ) { let connection: any; @@ -32,7 +32,7 @@ async function executeSqlQueryHandler( async function executeExplainSqlHandler( _: any, - config: MysqlConnection, + config: AppConnection, query: string ) { let connection: any; @@ -96,6 +96,6 @@ async function executeExplainSqlHandler( } export function registerSqlExecutorHandlers() { - ipcMain.handle('executeSqlQuery', executeSqlQueryHandler); - ipcMain.handle('executeExplainSql', executeExplainSqlHandler); + ipcMain.handle('execute-sql-query', executeSqlQueryHandler); + ipcMain.handle('execute-explain-sql', executeExplainSqlHandler); } diff --git a/electron/modules/ssh.ts b/electron/modules/ssh.ts new file mode 100644 index 0000000..57ba20f --- /dev/null +++ b/electron/modules/ssh.ts @@ -0,0 +1,162 @@ +import { ipcMain } from 'electron'; +import * as path from 'path'; +import { + testConnection, + closeConnection, + executeCommand, + readRemoteFile, + writeRemoteFile, + listRemoteFiles, + createTunnel, + closeTunnel +} from '../helpers/ssh'; +import { SshConnection } from '../../src/types/ssh-connection'; + +function registerSshHandlers() { + ipcMain.handle('ssh:test-connection', async (_, config: SshConnection) => { + return await testConnection(config); + }); + + ipcMain.handle( + 'ssh:execute-command', + async (_, config: SshConnection, command: string) => { + return await executeCommand(config, command); + } + ); + + ipcMain.handle( + 'ssh:read-file', + async (_, config: SshConnection, filePath: string) => { + try { + const content = await readRemoteFile(config, filePath); + return { success: true, content: content.toString('utf-8') }; + } catch (error) { + return { + success: false, + error: + error instanceof Error + ? error.message + : 'Unknown error reading file' + }; + } + } + ); + + ipcMain.handle( + 'ssh:write-file', + async (_, config: SshConnection, filePath: string, content: string) => { + try { + await writeRemoteFile(config, filePath, content); + return { success: true }; + } catch (error) { + return { + success: false, + error: + error instanceof Error + ? error.message + : 'Unknown error writing file' + }; + } + } + ); + + ipcMain.handle( + 'ssh:list-files', + async (_, config: SshConnection, dirPath: string) => { + try { + const files = await listRemoteFiles(config, dirPath); + return { success: true, files }; + } catch (error) { + return { + success: false, + error: + error instanceof Error + ? error.message + : 'Unknown error listing files' + }; + } + } + ); + + ipcMain.handle('ssh:get-env', async (_, config: SshConnection) => { + try { + const envPath = path.join(config.remotePath, '.env'); + const content = await readRemoteFile(config, envPath); + + return { success: true, content: content.toString('utf-8') }; + } catch (error) { + return { + success: false, + error: + error instanceof Error + ? error.message + : 'Unknown error reading .env file' + }; + } + }); + + ipcMain.handle( + 'ssh:update-env', + async (_, config: SshConnection, content: string) => { + try { + const envPath = path.join(config.remotePath, '.env'); + await writeRemoteFile(config, envPath, content); + + return { success: true }; + } catch (error) { + return { + success: false, + error: + error instanceof Error + ? error.message + : 'Unknown error updating .env file' + }; + } + } + ); + + ipcMain.handle('ssh:close-connection', (_, config: SshConnection) => { + closeConnection(config); + return { success: true }; + }); + + ipcMain.handle( + 'ssh:create-tunnel', + async ( + _, + config: SshConnection, + remoteHost: string, + remotePort: number, + localPort?: number + ) => { + try { + const result = await createTunnel( + config, + remoteHost, + remotePort, + localPort + ); + return { + success: true, + tunnelId: result.tunnelId, + localPort: result.localPort + }; + } catch (error) { + return { + success: false, + error: + error instanceof Error + ? error.message + : 'Unknown error creating tunnel' + }; + } + } + ); + + ipcMain.handle('ssh:close-tunnel', (_, tunnelId: string) => { + const success = closeTunnel(tunnelId); + return { success }; + }); +} + +export { registerSshHandlers }; diff --git a/electron/modules/tables.ts b/electron/modules/tables.ts index 6e5a374..1e27dcc 100644 --- a/electron/modules/tables.ts +++ b/electron/modules/tables.ts @@ -1,5 +1,4 @@ import { ipcMain } from 'electron'; -import { MysqlConnection } from '../../src/types/mysql-connection'; import { createConnection, safeEndConnection } from '../helpers/mysql'; import { BASE_COUNT_SQL, @@ -19,6 +18,40 @@ import { TableRecord, UpdateTableRecord } from '../../src/types/table'; +import { AppConnection } from '../../src/types/ssh-connection'; +import { PoolConnection, RowDataPacket, ResultSetHeader } from 'mysql2/promise'; + +interface TableRow extends RowDataPacket { + name: string; + rowCount?: number; + isApproximate?: boolean; +} + +interface TableCountRow extends RowDataPacket { + name: string; + rowCount: number; +} + +interface CountResult extends RowDataPacket { + count: number; +} + +interface TableColumn extends RowDataPacket { + name: string; + type?: string; + foreign_key?: boolean; +} + +interface ForeignKeyRow extends RowDataPacket { + type?: 'outgoing' | 'incoming'; +} + +interface IndexRow extends RowDataPacket { + name: string; + columns?: string | string[]; + is_unique?: number; + type: string; +} function error(type: string, message: string) { const base = { success: false, message }; @@ -33,15 +66,58 @@ function error(type: string, message: string) { return (map[type] || map.default)(); } -async function toggleForeignKeyChecks(connection: any, enable: boolean) { +function getDatabaseName(config: AppConnection): string { + return config.remote + ? config.remote.remoteDbConfig.database + : config.localDbConfig.database; +} + +async function toggleForeignKeyChecks( + connection: PoolConnection, + enable: boolean +): Promise { await connection.query(`SET FOREIGN_KEY_CHECKS = ${enable ? 1 : 0}`); } +async function withTransaction( + connection: PoolConnection, + callback: () => Promise, + disableForeignKeys = false +): Promise { + let fkDisabled = false; + + try { + await connection.beginTransaction(); + + if (disableForeignKeys) { + await toggleForeignKeyChecks(connection, false); + fkDisabled = true; + } + + const result = await callback(); + + if (fkDisabled) { + await toggleForeignKeyChecks(connection, true); + } + + await connection.commit(); + return result; + } catch (err) { + await connection.rollback().catch(() => {}); + + if (fkDisabled) { + await toggleForeignKeyChecks(connection, true).catch(() => {}); + } + + throw err; + } +} + function applySortingAndPagination( baseSql: string, - connection: any, + connection: PoolConnection, config: TableRecord -) { +): string { let sql = baseSql; if (config.sortColumn) { @@ -55,29 +131,102 @@ function applySortingAndPagination( } async function getTableStructure( - connection: any, + connection: PoolConnection, database: string, tableName: string -) { - const [cols] = await connection.query(COLUMN_SQL, [database, tableName]); - const [fks] = await connection.query(FK_SQL, [database, tableName]); - const fkSet = new Set(fks.map((f: any) => f.column_name)); +): Promise { + const [cols] = await connection.query(COLUMN_SQL, [ + database, + tableName + ]); + const [fks] = await connection.query(FK_SQL, [ + database, + tableName + ]); + const fkSet = new Set(fks.map((f) => f.column_name)); + + return cols.map((c) => { + return { + ...(c as unknown as Record), + name: c.name as string, + type: c.type as string, + foreign_key: fkSet.has(c.name as string) + } as TableColumn; + }); +} + +async function getTableForeignKeys( + connection: PoolConnection, + database: string, + tableName: string +): Promise { + const [out] = await connection.query(OUTGOING_SQL, [ + database, + tableName + ]); + const [inc] = await connection.query(INCOMING_SQL, [ + database, + tableName + ]); + + return [ + ...out.map((fk) => ({ ...fk, type: 'outgoing' as const })), + ...inc.map((fk) => ({ ...fk, type: 'incoming' as const })) + ]; +} + +function processIndexes(indexes: IndexRow[]): IndexRow[] { + return indexes.map((index) => { + const result = { ...index }; - return cols.map((c: any) => ({ ...c, foreign_key: fkSet.has(c.name) })); + if (result.columns && typeof result.columns === 'string') { + (result.columns as unknown) = (result.columns as string).split(','); + } else if (!result.columns) { + (result.columns as unknown) = []; + } + + if (result.name === 'PRIMARY') { + result.type = 'PRIMARY'; + } else if (result.is_unique === 0) { + result.type = 'UNIQUE'; + } else if (result.type === 'FULLTEXT') { + result.type = 'FULLTEXT'; + } else if (result.type === 'SPATIAL') { + result.type = 'SPATIAL'; + } else { + result.type = 'INDEX'; + } + + delete result.is_unique; + return result; + }); +} + +async function getTableIndexes( + connection: PoolConnection, + database: string, + tableName: string +): Promise { + const [indexes] = await connection.query(TABLE_INDEXES_SQL, [ + database, + tableName + ]); + + return processIndexes(indexes); } async function findRestrictingIds( - connection: any, + connection: PoolConnection, tableName: string, ids: (string | number)[] ): Promise<(string | number)[]> { - const [constraints]: any[] = await connection.query( + const [constraints] = await connection.query( REFERENCE_CONSTRAINTS_SQL, - [tableName, connection.database] + [tableName, connection.config.database] ); - const restricts = constraints.filter((c: any) => - ['RESTRICT', 'NO ACTION'].includes(c.on_delete) + const restricts = constraints.filter((c: RowDataPacket) => + ['RESTRICT', 'NO ACTION'].includes(c.on_delete as string) ); const problematic: (string | number)[] = []; @@ -90,7 +239,7 @@ async function findRestrictingIds( WHERE ${connection.escapeId(child_column)} = ? LIMIT 1 `; - const [rows]: any[] = await connection.query(sql, [id]); + const [rows] = await connection.query(sql, [id]); if (rows.length) { problematic.push(id); break; @@ -102,29 +251,42 @@ async function findRestrictingIds( } async function deleteRecords( - connection: any, + connection: PoolConnection, tableName: string, ids: (string | number)[] ): Promise<{ affectedRows: number }> { const placeholders = ids.map(() => '?').join(','); + + // noinspection SqlResolve const sql = ` DELETE FROM ${connection.escapeId(tableName)} WHERE id IN (${placeholders}) `; - const [result]: any[] = await connection.execute(sql, ids); + const [result] = await connection.execute(sql, ids); return { affectedRows: result.affectedRows }; } -async function listTablesHandler(_: any, config: MysqlConnection) { - let connection: any; +async function listTablesHandler(_: unknown, config: AppConnection) { + let connection: PoolConnection | null = null; + + const db = + config.localDbConfig || (config.remote && config.remote.remoteDbConfig); + + if (!db) { + return { + success: false, + message: 'No valid database configuration found', + tables: [] + }; + } try { connection = await createConnection(config); - const [tables] = await connection.query(LIST_TABLES_SQL, [ - config.database + const [tables] = await connection.query(LIST_TABLES_SQL, [ + db.database ]); if (tables.length === 0) { @@ -132,118 +294,107 @@ async function listTablesHandler(_: any, config: MysqlConnection) { } try { - const [info] = await connection.query(BASE_COUNT_SQL, [ - config.database - ]); + const [info] = await connection.query( + BASE_COUNT_SQL, + [db.database] + ); const counts = Object.fromEntries( - info.map(({ name, rowCount }: any) => [name, +rowCount || 0]) + info.map(({ name, rowCount }) => [name, +rowCount || 0]) ); - tables.forEach((t: any) => { + tables.forEach((t) => { t.rowCount = counts[t.name] || 0; t.isApproximate = true; }); } catch (e) { console.error('Error fetching counts:', e); - tables.forEach((t: any) => { + tables.forEach((t) => { t.rowCount = 0; t.isApproximate = true; }); } return { success: true, tables }; - } catch (err: any) { - return error('tables', err.message); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + return error('tables', errorMessage); } finally { await safeEndConnection(connection); } } async function getTableRecordCountHandler( - _: any, - config: MysqlConnection, + _: unknown, + config: AppConnection, table: Table ) { - let connection: any; + let connection: PoolConnection | null = null; try { connection = await createConnection(config); const escaped = connection.escapeId(table.name); - const [rows] = await connection.query( + const [rows] = await connection.query( `SELECT COUNT(*) AS count FROM ${escaped}` ); return rows.length ? { success: true, count: rows[0].count || 0, isApproximate: false } : error('count', 'Failed to count records'); - } catch (err: any) { - return error('count', err.message); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + return error('count', errorMessage); } finally { await safeEndConnection(connection); } } -async function dropTablesHandler(_: any, params: DropTableParams) { - let connection: any; +async function dropTablesHandler(_: unknown, params: DropTableParams) { + let connection: PoolConnection | null = null; try { - connection = await createConnection(params.dbConnection); - await connection.query('START TRANSACTION'); - - if (params.ignoreForeignKeys) { - await toggleForeignKeyChecks(connection, false); - } - - const failed: string[] = []; - - let successCount = 0; - - for (const name of params.tables) { - try { - const esc = connection.escapeId(name); - const cascade = params.cascade ? ' CASCADE' : ''; - - await connection.query(`DROP TABLE${cascade} ${esc}`); - successCount++; - } catch (e: any) { - console.error(`Drop failed for ${name}:`, e.message); - failed.push(name); + connection = await createConnection(params.appConnection); + + const dropTables = async () => { + const failed: string[] = []; + let successCount = 0; + + for (const name of params.tables) { + try { + const esc = connection.escapeId(name); + const cascade = params.cascade ? ' CASCADE' : ''; + + await connection.query(`DROP TABLE${cascade} ${esc}`); + successCount++; + } catch (e) { + const errorMessage = + e instanceof Error ? e.message : String(e); + console.error(`Drop failed for ${name}:`, errorMessage); + failed.push(name); + } } - } - if (params.ignoreForeignKeys) { - await toggleForeignKeyChecks(connection, true); - } + if (failed.length > 0) { + throw new Error(`Failed to drop tables: ${failed.join(', ')}`); + } - if (failed.length === 0) { - await connection.query('COMMIT'); + return { successCount }; + }; - return { - success: true, - message: `Dropped ${successCount} tables successfully` - }; - } else { - await connection.query('ROLLBACK'); + const { successCount } = await withTransaction( + connection, + dropTables, + params.ignoreForeignKeys + ); - return { - success: false, - message: `Failed to drop tables: ${failed.join(', ')}` - }; - } - } catch (err: any) { + return { + success: true, + message: `Dropped ${successCount} tables successfully` + }; + } catch (err) { console.error('Error in dropTablesHandler:', err); - - if (connection) { - try { - await connection.query('ROLLBACK'); - if (params.ignoreForeignKeys) { - await toggleForeignKeyChecks(connection, true); - } - } catch {} - } - + const errorMessage = err instanceof Error ? err.message : String(err); return { success: false, - message: err.message || 'Error dropping tables' + message: errorMessage || 'Error dropping tables' }; } finally { await safeEndConnection(connection); @@ -251,70 +402,60 @@ async function dropTablesHandler(_: any, params: DropTableParams) { } async function truncateTableHandler( - _: any, - config: MysqlConnection, + _: unknown, + config: AppConnection, tableName: string ) { - let connection: any; - let fkDisabled = false; + let connection: PoolConnection | null = null; try { connection = await createConnection(config); + const database = getDatabaseName(config); - const [fkRes] = await connection.query(HAS_FK_USAGE_SQL, [ - tableName, - config.database - ]); - - fkDisabled = fkRes.length > 0; + const [fkRes] = await connection.query( + HAS_FK_USAGE_SQL, + [tableName, database] + ); - await connection.beginTransaction(); + const hasFkReferences = fkRes.length > 0; - if (fkDisabled) { - await toggleForeignKeyChecks(connection, false); - } - await connection.query( - `TRUNCATE TABLE ${connection.escapeId(tableName)}` + await withTransaction( + connection, + async () => { + await connection.query( + `TRUNCATE TABLE ${connection.escapeId(tableName)}` + ); + return true; + }, + hasFkReferences ); - if (fkDisabled) { - await toggleForeignKeyChecks(connection, true); - } - - await connection.commit(); return { success: true, message: `Table ${tableName} truncated successfully` }; - } catch (err: any) { + } catch (err) { console.error(`Error truncating ${tableName}:`, err); - - if (connection) { - await connection.rollback().catch(() => {}); - - if (fkDisabled) { - await toggleForeignKeyChecks(connection, true).catch(() => {}); - } - } - + const errorMessage = err instanceof Error ? err.message : String(err); return { success: false, - message: err.message || 'Error truncating table' + message: errorMessage || 'Error truncating table' }; } finally { await safeEndConnection(connection); } } -async function getTableRecordsHandler(_: any, config: TableRecord) { - let connection: any; +async function getTableRecordsHandler(_: unknown, config: TableRecord) { + let connection: PoolConnection | null = null; try { - connection = await createConnection(config.dbConnection); + connection = await createConnection(config.appConnection); + const database = getDatabaseName(config.appConnection); const structure = await getTableStructure( connection, - config.dbConnection.database, + database, config.tableName ); @@ -334,13 +475,21 @@ async function getTableRecordsHandler(_: any, config: TableRecord) { const countQuery = `SELECT COUNT(*) AS totalRecords FROM ${tableNameEsc}${where}`; try { - const [countRows] = await connection.query(countQuery); + interface TotalRecordsRow extends RowDataPacket { + totalRecords: number; + } + + const [countRows] = + await connection.query(countQuery); const totalRecords = countRows[0]?.totalRecords || 0; const baseSql = `SELECT * FROM ${tableNameEsc}${where}`; const sql = applySortingAndPagination(baseSql, connection, config); - const [rows] = await connection.query(sql, [limit, offset]); + const [rows] = await connection.query(sql, [ + limit, + offset + ]); return { success: true, @@ -350,12 +499,19 @@ async function getTableRecordsHandler(_: any, config: TableRecord) { limit, structure }; - } catch (queryError: any) { + } catch (queryError) { console.error('SQL Error:', queryError); + interface SqlError extends Error { + code?: string; + sqlMessage?: string; + } + + const sqlError = queryError as SqlError; + if ( - queryError.code === 'ER_PARSE_ERROR' && - queryError.sqlMessage.includes('order') + sqlError.code === 'ER_PARSE_ERROR' && + sqlError.sqlMessage?.includes('order') ) { return { success: false, @@ -368,10 +524,11 @@ async function getTableRecordsHandler(_: any, config: TableRecord) { throw queryError; } - } catch (err: any) { + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); return { success: false, - message: err.message || 'Error fetching records', + message: errorMessage || 'Error fetching records', data: [], totalRecords: 0 }; @@ -381,18 +538,18 @@ async function getTableRecordsHandler(_: any, config: TableRecord) { } async function getTableStructureHandler( - _: any, - config: MysqlConnection, + _: unknown, + config: AppConnection, tableName: string ) { - let connection: any; + let connection: PoolConnection | null = null; try { connection = await createConnection(config); - + const database = getDatabaseName(config); const structure = await getTableStructure( connection, - config.database, + database, tableName ); @@ -400,10 +557,11 @@ async function getTableStructureHandler( success: true, structure }; - } catch (err: any) { + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); return { success: false, - message: err.message || 'Error fetching records' + message: errorMessage || 'Error fetching records' }; } finally { await safeEndConnection(connection); @@ -411,42 +569,38 @@ async function getTableStructureHandler( } async function getTableForeignKeysHandler( - _: any, - config: MysqlConnection, + _: unknown, + config: AppConnection, tableName: string ) { - let connection: any; + let connection: PoolConnection | null = null; try { connection = await createConnection(config); + const database = getDatabaseName(config); - const [out] = await connection.query(OUTGOING_SQL, [ - config.database, - tableName - ]); - const [inc] = await connection.query(INCOMING_SQL, [ - config.database, + const foreignKeys = await getTableForeignKeys( + connection, + database, tableName - ]); + ); return { success: true, - foreignKeys: [ - ...out.map((fk: any) => ({ ...fk, type: 'outgoing' })), - ...inc.map((fk: any) => ({ ...fk, type: 'incoming' })) - ] + foreignKeys }; } catch (err) { - return error('foreign', err.message || 'Failed to fetch foreign keys'); + const errorMessage = err instanceof Error ? err.message : String(err); + return error('foreign', errorMessage || 'Failed to fetch foreign keys'); } finally { await safeEndConnection(connection); } } async function deleteTableRecordsHandler( - _: any, + _: unknown, { - dbConnection: config, + appConnection: config, tableName, ids, ignoreForeignKeys @@ -456,7 +610,7 @@ async function deleteTableRecordsHandler( throw new Error('At least one record ID is required'); } - let connection: any; + let connection: PoolConnection | null = null; try { connection = await createConnection(config); @@ -498,8 +652,14 @@ async function deleteTableRecordsHandler( message: `${affectedRows} record(s) deleted successfully`, affectedRows }; - } catch (err: any) { - if (err.code === 'ER_ROW_IS_REFERENCED_2') { + } catch (err) { + interface SqlError extends Error { + code?: string; + } + + const sqlError = err as SqlError; + + if (sqlError.code === 'ER_ROW_IS_REFERENCED_2') { return { success: false, message: @@ -508,11 +668,12 @@ async function deleteTableRecordsHandler( }; } - return { success: false, message: err.message }; + const errorMessage = err instanceof Error ? err.message : String(err); + return { success: false, message: errorMessage }; } finally { if (connection) { if (ignoreForeignKeys) { - await toggleForeignKeyChecks(connection, true).catch((e: any) => + await toggleForeignKeyChecks(connection, true).catch((e) => console.error('Error re-enabling foreign key checks:', e) ); } @@ -522,12 +683,12 @@ async function deleteTableRecordsHandler( } } -async function updateRecordHandler(_: any, config: UpdateTableRecord) { - let connection: any; +async function updateRecordHandler(_: unknown, config: UpdateTableRecord) { + let connection: PoolConnection | null = null; try { - connection = await createConnection(config.dbConnection); - + connection = await createConnection(config.appConnection); + const database = getDatabaseName(config.appConnection); const { tableName, data, id } = config; if (!tableName || !data || !id) { @@ -544,12 +705,12 @@ async function updateRecordHandler(_: any, config: UpdateTableRecord) { const structure = await getTableStructure( connection, - config.dbConnection.database, + database, tableName ); const jsonColumns = structure - .filter((col: any) => col.type?.toLowerCase().includes('json')) - .map((col: any) => col.name); + .filter((col) => col.type?.toLowerCase().includes('json')) + .map((col) => col.name); const processedData = Object.fromEntries( Object.entries(updateData).map(([key, value]) => { @@ -557,7 +718,13 @@ async function updateRecordHandler(_: any, config: UpdateTableRecord) { try { JSON.parse(value); return [key, value]; - } catch (e) { + } catch (e: unknown) { + console.error( + `Invalid JSON for column ${key}:`, + e, + value + ); + return [key, value]; } } @@ -569,22 +736,24 @@ async function updateRecordHandler(_: any, config: UpdateTableRecord) { .map(([key, _]) => `${connection.escapeId(key)} = ?`) .join(', '); + // noinspection SqlResolve const sql = `UPDATE ${tableNameEsc} SET ${setClause} WHERE id = ?`; const values = [...Object.values(processedData), id]; - const [result] = await connection.execute(sql, values); + const [result] = await connection.execute(sql, values); return { success: true, message: `Record updated successfully`, affectedRows: result.affectedRows }; - } catch (err: any) { + } catch (err) { console.error('Error updating record:', err); + const errorMessage = err instanceof Error ? err.message : String(err); return { success: false, - message: err.message || 'Error updating record' + message: errorMessage || 'Error updating record' }; } finally { await safeEndConnection(connection); @@ -592,50 +761,27 @@ async function updateRecordHandler(_: any, config: UpdateTableRecord) { } async function getTableIndexesHandler( - _: any, - config: MysqlConnection, + _: unknown, + config: AppConnection, tableName: string ) { - let connection: any; + let connection: PoolConnection | null = null; try { connection = await createConnection(config); + const database = getDatabaseName(config); - const [indexes] = await connection.query(TABLE_INDEXES_SQL, [ - config.database, - tableName - ]); - - indexes.forEach((index: any) => { - if (index.columns && typeof index.columns === 'string') { - index.columns = index.columns.split(','); - } else { - index.columns = []; - } - - if (index.name === 'PRIMARY') { - index.type = 'PRIMARY'; - } else if (index.is_unique === 0) { - index.type = 'UNIQUE'; - } else if (index.type === 'FULLTEXT') { - index.type = 'FULLTEXT'; - } else if (index.type === 'SPATIAL') { - index.type = 'SPATIAL'; - } else { - index.type = 'INDEX'; - } - - delete index.is_unique; - }); + const indexes = await getTableIndexes(connection, database, tableName); return { success: true, indexes }; - } catch (err: any) { + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); return { success: false, - message: err.message || 'Error fetching table indexes', + message: errorMessage || 'Error fetching table indexes', indexes: [] }; } finally { @@ -643,14 +789,18 @@ async function getTableIndexesHandler( } } -async function getDatabaseSchemaForAIHandler(_: any, config: MysqlConnection) { - let connection: any; +async function getDatabaseSchemaForAIHandler( + _: unknown, + config: AppConnection +) { + let connection: PoolConnection | null = null; try { connection = await createConnection(config); + const database = getDatabaseName(config); - const [tables] = await connection.query(LIST_TABLES_SQL, [ - config.database + const [tables] = await connection.query(LIST_TABLES_SQL, [ + database ]); if (tables.length === 0) { @@ -664,50 +814,21 @@ async function getDatabaseSchemaForAIHandler(_: any, config: MysqlConnection) { const structure = await getTableStructure( connection, - config.database, + database, tableName ); - const [outgoing] = await connection.query(OUTGOING_SQL, [ - config.database, - tableName - ]); - const [incoming] = await connection.query(INCOMING_SQL, [ - config.database, + const foreignKeys = await getTableForeignKeys( + connection, + database, tableName - ]); - - const foreignKeys = [ - ...outgoing.map((fk: any) => ({ ...fk, type: 'outgoing' })), - ...incoming.map((fk: any) => ({ ...fk, type: 'incoming' })) - ]; + ); - const [indexes] = await connection.query(TABLE_INDEXES_SQL, [ - config.database, + const indexes = await getTableIndexes( + connection, + database, tableName - ]); - - indexes.forEach((index: any) => { - if (index.columns && typeof index.columns === 'string') { - index.columns = index.columns.split(','); - } else { - index.columns = []; - } - - if (index.name === 'PRIMARY') { - index.type = 'PRIMARY'; - } else if (index.is_unique === 0) { - index.type = 'UNIQUE'; - } else if (index.type === 'FULLTEXT') { - index.type = 'FULLTEXT'; - } else if (index.type === 'SPATIAL') { - index.type = 'SPATIAL'; - } else { - index.type = 'INDEX'; - } - - delete index.is_unique; - }); + ); databaseSchema.tables.push({ name: tableName, @@ -721,10 +842,11 @@ async function getDatabaseSchemaForAIHandler(_: any, config: MysqlConnection) { success: true, databaseSchema }; - } catch (err: any) { + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); return { success: false, - message: err.message || 'Error fetching database schema', + message: errorMessage || 'Error fetching database schema', databaseSchema: { tables: [] } }; } finally { @@ -732,7 +854,7 @@ async function getDatabaseSchemaForAIHandler(_: any, config: MysqlConnection) { } } -function registerTablesHandlers() { +function registerTablesHandlers(): void { ipcMain.handle('list-tables', listTablesHandler); ipcMain.handle('get-table-record-count', getTableRecordCountHandler); ipcMain.handle('drop-tables', dropTablesHandler); diff --git a/electron/preload/index.ts b/electron/preload/index.ts index ad60a49..1431eb4 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -1,6 +1,5 @@ import { ipcRenderer, contextBridge } from 'electron'; import { Settings } from '../../src/types/settings'; -import { MysqlConnection } from '../../src/types/mysql-connection'; import { RestoreConfig } from '../../src/types/project'; import { RedisConnection } from '../../src/types/redis'; import { @@ -10,6 +9,7 @@ import { TableRecord, UpdateTableRecord } from '../../src/types/table'; +import { AppConnection, SshConnection } from '../../src/types/ssh-connection'; contextBridge.exposeInMainWorld('ipcRenderer', { on(...args: Parameters) { @@ -36,12 +36,23 @@ contextBridge.exposeInMainWorld('ipcRenderer', { const [channel] = args; return ipcRenderer.removeAllListeners(channel); }, + /** + * Window Management + */ + openConnectionWindow: (connectionId: string, isRemote: boolean) => + ipcRenderer.invoke('open-connection-window', connectionId, isRemote), + closeConnectionWindow: (connectionId: string) => + ipcRenderer.invoke('close-connection-window', connectionId), + openSqlEditorWindow: (connectionId: string, isRemote: boolean) => + ipcRenderer.invoke('open-sql-editor-window', connectionId, isRemote), + showHomeWindow: () => ipcRenderer.invoke('show-home-window'), + getWindowId: () => ipcRenderer.invoke('get-window-id'), /** * DB monitoring */ startLiveDbUpdate: (config: { connectionId: string; - dbConnection: MysqlConnection; + appConnection: AppConnection; clearHistory?: boolean; }) => ipcRenderer.invoke('start-live-db-updates', config), stopDbMonitoring: (connectionId: string) => @@ -90,36 +101,36 @@ contextBridge.exposeInMainWorld('ipcRenderer', { /** * MySQL */ - testMySQLConnection: (config: MysqlConnection) => + testMySQLConnection: (config: AppConnection) => ipcRenderer.invoke('test-mysql-connection', config), - listDatabases: (config: MysqlConnection) => + listDatabases: (config: AppConnection) => ipcRenderer.invoke('list-databases', config), - dropDatabase: (config: MysqlConnection, databaseName: string) => + dropDatabase: (config: AppConnection, databaseName: string) => ipcRenderer.invoke('drop-database', config, databaseName), - createDatabase: (config: MysqlConnection, databaseName: string) => + createDatabase: (config: AppConnection, databaseName: string) => ipcRenderer.invoke('create-database', config, databaseName), /** * Tables */ dropTables: (params: DropTableParams) => ipcRenderer.invoke('drop-tables', params), - listTables: (config: MysqlConnection) => + listTables: (config: AppConnection) => ipcRenderer.invoke('list-tables', config), - getTableRecordCount: (config: MysqlConnection, table: Table) => + getTableRecordCount: (config: AppConnection, table: Table) => ipcRenderer.invoke('get-table-record-count', config, table), - truncateTable: (config: MysqlConnection, tableName: string) => + truncateTable: (config: AppConnection, tableName: string) => ipcRenderer.invoke('truncate-table', config, tableName), getTableRecords: (config: TableRecord) => ipcRenderer.invoke('get-table-records', config), deleteRecords: (config: DeleteRowsConfig) => ipcRenderer.invoke('delete-table-records', config), - getTableStructure: (config: MysqlConnection, tableName: string) => + getTableStructure: (config: AppConnection, tableName: string) => ipcRenderer.invoke('get-table-structure', config, tableName), - getTableForeignKeys: (config: MysqlConnection, tableName: string) => + getTableForeignKeys: (config: AppConnection, tableName: string) => ipcRenderer.invoke('get-table-foreign-keys', config, tableName), - getTableIndexes: (config: MysqlConnection, tableName: string) => + getTableIndexes: (config: AppConnection, tableName: string) => ipcRenderer.invoke('get-table-indexes', config, tableName), - getDatabaseSchemaForAI: (config: MysqlConnection) => + getDatabaseSchemaForAI: (config: AppConnection) => ipcRenderer.invoke('get-database-schema-for-ai', config), updateTableRecord: (config: UpdateTableRecord) => ipcRenderer.invoke('update-table-record', config), @@ -131,13 +142,13 @@ contextBridge.exposeInMainWorld('ipcRenderer', { getMigrationStatus: (config: { projectPath: string; usingSail: boolean; - db_config: MysqlConnection; + appConnection: AppConnection; }) => ipcRenderer.invoke('get-migration-status', config), /** * Files */ readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath), - saveFile: (filePath: string, content: any) => + saveFile: (filePath: string, content: string) => ipcRenderer.invoke('save-file', filePath, content), /** * Project @@ -154,6 +165,7 @@ contextBridge.exposeInMainWorld('ipcRenderer', { findModelsForTables: (projectPath: string) => ipcRenderer.invoke('find-models-for-tables', projectPath), selectDirectory: () => ipcRenderer.invoke('select-directory'), + selectFile: (options: object) => ipcRenderer.invoke('select-file', options), validateLaravelProject: (projectPath: string) => ipcRenderer.invoke('validate-laravel-project', projectPath), readEnvFile: (projectPath: string) => @@ -196,15 +208,18 @@ contextBridge.exposeInMainWorld('ipcRenderer', { /** * SQL Executor */ - executeSqlQuery: (config: MysqlConnection, query: string) => - ipcRenderer.invoke('executeSqlQuery', config, query), - executeExplainSql: (config: MysqlConnection, query: string) => - ipcRenderer.invoke('executeExplainSql', config, query), + executeSqlQuery: (config: AppConnection, query: string) => + ipcRenderer.invoke('execute-sql-query', config, query), + executeExplainSql: (config: AppConnection, query: string) => + ipcRenderer.invoke('execute-explain-sql', config, query), /** * Auto-updater */ onUpdateStatus: (callback: Function) => { - const subscription = (_event: any, ...args: any[]) => callback(...args); + const subscription = ( + _event: Electron.IpcRendererEvent, + ...args: unknown[] + ) => callback(...args); ipcRenderer.on('update-status', subscription); return () => { ipcRenderer.removeListener('update-status', subscription); @@ -214,7 +229,64 @@ contextBridge.exposeInMainWorld('ipcRenderer', { downloadUpdate: () => ipcRenderer.invoke('download-update'), quitAndInstall: () => ipcRenderer.invoke('quit-and-install'), getCurrentVersion: () => ipcRenderer.invoke('get-current-version'), - openExternal: (url: string) => ipcRenderer.invoke('open-external', url) + openExternal: (url: string) => ipcRenderer.invoke('open-external', url), + + /** + * SSH Operations + */ + ssh: { + testConnection: (config: SshConnection) => + ipcRenderer.invoke('ssh:test-connection', config), + executeCommand: (config: SshConnection, command: string) => + ipcRenderer.invoke('ssh:execute-command', config, command), + readFile: (config: SshConnection, filePath: string) => + ipcRenderer.invoke('ssh:read-file', config, filePath), + writeFile: (config: SshConnection, filePath: string, content: string) => + ipcRenderer.invoke('ssh:write-file', config, filePath, content), + listFiles: (config: SshConnection, dirPath: string) => + ipcRenderer.invoke('ssh:list-files', config, dirPath), + getEnv: (config: SshConnection) => + ipcRenderer.invoke('ssh:get-env', config), + updateEnv: (config: SshConnection, content: string) => + ipcRenderer.invoke('ssh:update-env', config, content), + closeConnection: (config: SshConnection) => + ipcRenderer.invoke('ssh:close-connection', config), + createTunnel: ( + config: SshConnection, + remoteHost: string, + remotePort: number, + localPort?: number + ) => + ipcRenderer.invoke( + 'ssh:create-tunnel', + config, + remoteHost, + remotePort, + localPort + ), + closeTunnel: (tunnelId: string) => + ipcRenderer.invoke('ssh:close-tunnel', tunnelId) + }, + + /** + * SSH Tunneling + */ + findAvailablePort: (min: number, max: number) => + ipcRenderer.invoke('find-available-port', { min, max }), + createSSHTunnel: (config: { + sshConfig: SshConnection; + localPort: number; + remoteHost: string; + remotePort: number; + }) => ipcRenderer.invoke('create-ssh-tunnel', config), + closeSSHTunnel: (tunnel: { server: unknown; connection: unknown }) => + ipcRenderer.invoke('close-ssh-tunnel', tunnel), + + /** + * App Icon Badge + */ + updateMigrationsBadge: (count: number) => + ipcRenderer.invoke('update-migrations-badge', count) }); function domReady( @@ -298,7 +370,7 @@ function useLoading() { const { appendLoading, removeLoading } = useLoading(); domReady().then(appendLoading); -window.onmessage = (ev) => { +window.onmessage = (ev: MessageEvent<{ payload: string }>) => { ev.data.payload === 'removeLoading' && removeLoading(); }; diff --git a/eslint.config.js b/eslint.config.js index 6a6596f..49a9c97 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,39 +3,82 @@ import globals from 'globals'; import typescript from '@typescript-eslint/eslint-plugin'; import typescriptParser from '@typescript-eslint/parser'; import vue from 'eslint-plugin-vue'; +import unusedImports from 'eslint-plugin-unused-imports'; export default [ - // Base JS config - js.configs.recommended, - - // Global settings for all files { - languageOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - globals: { - ...globals.node, - ...globals.browser - } - }, ignores: [ 'dist/**', 'dist-electron/**', + 'dist-electron/main/**', + 'dist-electron/preload/**', 'node_modules/**', '.vscode/**', '.idea/**', 'coverage/**', 'build/**', - '*.min.js' + '*.min.js', + 'release/**', + 'src/components/**/*.vue', + 'src/views/**/*.vue', + 'src/App.vue' + // '*.vue', // Uncomment to lint .vue files after fixing template parsing ] }, - // Vue and TS config for .vue files + js.configs.recommended, + + // Plugin to remove unused imports/vars in JS files { - files: ['**/*.vue'], plugins: { - vue + 'unused-imports': unusedImports }, + rules: { + // disable core rule to allow plugin to handle it + 'no-unused-vars': 'off', + 'no-throw-literal': 'off', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ + 'warn', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_' + } + ] + } + }, + + { + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.node, + ...globals.browser, + console: true, + setTimeout: true, + clearTimeout: true, + setInterval: true, + clearInterval: true, + process: true, + global: true, + setImmediate: true, + window: true, + document: true, + require: true, + Buffer: true, + AbortController: true, + AbortSignal: true, + performance: true + } + } + }, + + { + files: ['**/*.vue'], + plugins: { vue }, languageOptions: { parser: vue.configs['vue3-recommended'].parser, parserOptions: { @@ -43,9 +86,7 @@ export default [ ecmaVersion: 'latest', sourceType: 'module', extraFileExtensions: ['.vue'], - ecmaFeatures: { - jsx: true - } + ecmaFeatures: { jsx: true } } }, processor: vue.processors['.vue'], @@ -53,55 +94,71 @@ export default [ ...vue.configs['vue3-recommended'].rules, 'vue/multi-word-component-names': 'off', 'vue/require-default-prop': 'off', - 'vue/no-v-html': 'off' + 'vue/no-v-html': 'off', + 'vue/attributes-order': 'off', + 'vue/no-lone-template': 'off', + 'vue/this-in-template': 'off' } }, - // TypeScript config for .ts and .tsx files { files: ['**/*.ts', '**/*.tsx'], plugins: { - '@typescript-eslint': typescript + '@typescript-eslint': typescript, + 'unused-imports': unusedImports }, languageOptions: { parser: typescriptParser, parserOptions: { ecmaVersion: 'latest', sourceType: 'module', - ecmaFeatures: { - jsx: true - } + ecmaFeatures: { jsx: true } } }, rules: { ...typescript.configs.recommended.rules, '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': [ + // disable core and TS unused-vars rules + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ 'warn', - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_' - } + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' } ], - 'no-undef': 'off' // TypeScript already checks this + 'no-undef': 'off', + '@typescript-eslint/no-unsafe-function-type': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-unused-expressions': 'off' } }, - // Special rules for electron files { - files: ['electron/**/*.ts'], + files: ['electron/**/*.ts', 'dist-electron/**/*.{js,mjs}'], rules: { - 'no-console': 'off' + 'no-console': 'off', + 'no-undef': 'off' } }, - // General rules for all files { rules: { 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': - process.env.NODE_ENV === 'production' ? 'warn' : 'off' + process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-empty': 'warn', + 'no-case-declarations': 'off', + 'no-cond-assign': 'warn', + 'no-useless-escape': 'warn', + // remove no-unused-vars to avoid duplicate warnings + // 'no-unused-vars': 'warn', + 'no-prototype-builtins': 'warn', + 'no-control-regex': 'off', + 'no-redeclare': 'warn', + 'no-fallthrough': 'warn', + 'no-misleading-character-class': 'warn', + 'no-unreachable': 'warn' } } ]; diff --git a/package-lock.json b/package-lock.json index ecd08e3..2f62183 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "larabase", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "larabase", - "version": "1.0.0", + "version": "1.1.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -28,10 +28,13 @@ "monaco-editor": "^0.52.2", "mysql2": "^3.14.1", "openai": "^4.98.0", + "path-browserify": "^1.0.1", "pinia": "^3.0.2", "pluralize": "^8.0.0", + "portfinder": "^1.0.37", "redis": "^5.0.0", "sql-formatter": "^15.6.1", + "ssh2": "^1.16.0", "tailwindcss": "^4.1.4", "uuid": "^11.1.0", "vue-router": "^4.5.1" @@ -41,16 +44,18 @@ "@types/bcrypt": "^5.0.2", "@types/dockerode": "^3.3.38", "@types/lodash": "^4.17.16", + "@types/path-browserify": "^1.0.3", "@types/pluralize": "^0.0.33", + "@types/ssh2": "^1.15.0", "@typescript-eslint/eslint-plugin": "^8.31.1", "@typescript-eslint/parser": "^8.31.1", "@vitejs/plugin-vue": "^5.0.4", "daisyui": "^5.0.28", "electron": "^35.2.2", "electron-builder": "^26.0.14", - "electron-rebuild": "^3.2.9", "eslint": "^9.25.1", "eslint-config-prettier": "^10.1.2", + "eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-vue": "^9.33.0", "eslint-plugin-vue-pug": "^0.6.2", "globals": "^16.1.0", @@ -64,6 +69,10 @@ "vue": "^3.5.13", "vue-eslint-parser": "^9.4.3", "vue-tsc": "^2.2.10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-darwin-arm64": "^4.1.7", + "@tailwindcss/oxide-darwin-x64": "^4.1.7" } }, "node_modules/@alloc/quick-lru": { @@ -1241,9 +1250,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1325,13 +1334,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", - "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -1345,13 +1357,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { @@ -1824,28 +1836,6 @@ "node": ">=10" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz", - "integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.3", - "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2355,24 +2345,24 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz", - "integrity": "sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz", + "integrity": "sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", - "lightningcss": "1.29.2", + "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.6" + "tailwindcss": "4.1.7" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz", - "integrity": "sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.7.tgz", + "integrity": "sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -2383,24 +2373,24 @@ "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.6", - "@tailwindcss/oxide-darwin-arm64": "4.1.6", - "@tailwindcss/oxide-darwin-x64": "4.1.6", - "@tailwindcss/oxide-freebsd-x64": "4.1.6", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.6", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.6", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.6", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.6", - "@tailwindcss/oxide-linux-x64-musl": "4.1.6", - "@tailwindcss/oxide-wasm32-wasi": "4.1.6", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.6", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.6" + "@tailwindcss/oxide-android-arm64": "4.1.7", + "@tailwindcss/oxide-darwin-arm64": "4.1.7", + "@tailwindcss/oxide-darwin-x64": "4.1.7", + "@tailwindcss/oxide-freebsd-x64": "4.1.7", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.7", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.7", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.7", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.7", + "@tailwindcss/oxide-linux-x64-musl": "4.1.7", + "@tailwindcss/oxide-wasm32-wasi": "4.1.7", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.7", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.7" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.6.tgz", - "integrity": "sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.7.tgz", + "integrity": "sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==", "cpu": [ "arm64" ], @@ -2414,9 +2404,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.6.tgz", - "integrity": "sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.7.tgz", + "integrity": "sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==", "cpu": [ "arm64" ], @@ -2430,9 +2420,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.6.tgz", - "integrity": "sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.7.tgz", + "integrity": "sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==", "cpu": [ "x64" ], @@ -2446,9 +2436,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.6.tgz", - "integrity": "sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.7.tgz", + "integrity": "sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==", "cpu": [ "x64" ], @@ -2462,9 +2452,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.6.tgz", - "integrity": "sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.7.tgz", + "integrity": "sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==", "cpu": [ "arm" ], @@ -2478,9 +2468,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.6.tgz", - "integrity": "sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.7.tgz", + "integrity": "sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==", "cpu": [ "arm64" ], @@ -2494,9 +2484,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.6.tgz", - "integrity": "sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.7.tgz", + "integrity": "sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==", "cpu": [ "arm64" ], @@ -2510,9 +2500,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.6.tgz", - "integrity": "sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.7.tgz", + "integrity": "sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==", "cpu": [ "x64" ], @@ -2526,9 +2516,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.6.tgz", - "integrity": "sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.7.tgz", + "integrity": "sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==", "cpu": [ "x64" ], @@ -2542,9 +2532,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.6.tgz", - "integrity": "sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.7.tgz", + "integrity": "sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -2571,9 +2561,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.6.tgz", - "integrity": "sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.7.tgz", + "integrity": "sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==", "cpu": [ "arm64" ], @@ -2587,9 +2577,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.6.tgz", - "integrity": "sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.7.tgz", + "integrity": "sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==", "cpu": [ "x64" ], @@ -2603,16 +2593,16 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.6.tgz", - "integrity": "sha512-ELq+gDMBuRXPJlpE3PEen+1MhnHAQQrh2zF0dI1NXOlEWfr2qWf2CQdr5jl9yANv8RErQaQ2l6nIFO9OSCVq/g==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.7.tgz", + "integrity": "sha512-88g3qmNZn7jDgrrcp3ZXEQfp9CVox7xjP1HN2TFKI03CltPVd/c61ydn5qJJL8FYunn0OqBaW5HNUga0kmPVvw==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.6", - "@tailwindcss/oxide": "4.1.6", + "@tailwindcss/node": "4.1.7", + "@tailwindcss/oxide": "4.1.7", "postcss": "^8.4.41", - "tailwindcss": "4.1.6" + "tailwindcss": "4.1.7" } }, "node_modules/@tootallnate/once": { @@ -2737,9 +2727,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", - "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "version": "22.15.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", + "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -2755,6 +2745,13 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/path-browserify": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/path-browserify/-/path-browserify-1.0.3.tgz", + "integrity": "sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", @@ -3038,82 +3035,82 @@ } }, "node_modules/@volar/language-core": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.13.tgz", - "integrity": "sha512-MnQJ7eKchJx5Oz+YdbqyFUk8BN6jasdJv31n/7r6/WwlOOv7qzvot6B66887l2ST3bUW4Mewml54euzpJWA6bg==", + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.14.tgz", + "integrity": "sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.13" + "@volar/source-map": "2.4.14" } }, "node_modules/@volar/source-map": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.13.tgz", - "integrity": "sha512-l/EBcc2FkvHgz2ZxV+OZK3kMSroMr7nN3sZLF2/f6kWW66q8+tEL4giiYyFjt0BcubqJhBt6soYIrAPhg/Yr+Q==", + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.14.tgz", + "integrity": "sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.13.tgz", - "integrity": "sha512-Ukz4xv84swJPupZeoFsQoeJEOm7U9pqsEnaGGgt5ni3SCTa22m8oJP5Nng3Wed7Uw5RBELdLxxORX8YhJPyOgQ==", + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.14.tgz", + "integrity": "sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.13", + "@volar/language-core": "2.4.14", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "node_modules/@vue/compiler-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", - "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.14.tgz", + "integrity": "sha512-k7qMHMbKvoCXIxPhquKQVw3Twid3Kg4s7+oYURxLGRd56LiuHJVrvFKI4fm2AM3c8apqODPfVJGoh8nePbXMRA==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.27.2", + "@vue/shared": "3.5.14", "entities": "^4.5.0", "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", - "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.14.tgz", + "integrity": "sha512-1aOCSqxGOea5I80U2hQJvXYpPm/aXo95xL/m/mMhgyPUsKe9jhjwWpziNAw7tYRnbz1I61rd9Mld4W9KmmRoug==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-core": "3.5.14", + "@vue/shared": "3.5.14" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", - "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.14.tgz", + "integrity": "sha512-9T6m/9mMr81Lj58JpzsiSIjBgv2LiVoWjIVa7kuXHICUi8LiDSIotMpPRXYJsXKqyARrzjT24NAwttrMnMaCXA==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.13", - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.27.2", + "@vue/compiler-core": "3.5.14", + "@vue/compiler-dom": "3.5.14", + "@vue/compiler-ssr": "3.5.14", + "@vue/shared": "3.5.14", "estree-walker": "^2.0.2", - "magic-string": "^0.30.11", - "postcss": "^8.4.48", - "source-map-js": "^1.2.0" + "magic-string": "^0.30.17", + "postcss": "^8.5.3", + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", - "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.14.tgz", + "integrity": "sha512-Y0G7PcBxr1yllnHuS/NxNCSPWnRGH4Ogrp0tsLA5QemDZuJLs99YjAKQ7KqkHE0vCg4QTKlQzXLKCMF7WPSl7Q==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.14", + "@vue/shared": "3.5.14" } }, "node_modules/@vue/compiler-vue2": { @@ -3186,53 +3183,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", - "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.14.tgz", + "integrity": "sha512-7cK1Hp343Fu/SUCCO52vCabjvsYu7ZkOqyYu7bXV9P2yyfjUMUXHZafEbq244sP7gf+EZEz+77QixBTuEqkQQw==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.13" + "@vue/shared": "3.5.14" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", - "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.14.tgz", + "integrity": "sha512-w9JWEANwHXNgieAhxPpEpJa+0V5G0hz3NmjAZwlOebtfKyp2hKxKF0+qSh0Xs6/PhfGihuSdqMprMVcQU/E6ag==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/reactivity": "3.5.14", + "@vue/shared": "3.5.14" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", - "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.14.tgz", + "integrity": "sha512-lCfR++IakeI35TVR80QgOelsUIdcKjd65rWAMfdSlCYnaEY5t3hYwru7vvcWaqmrK+LpI7ZDDYiGU5V3xjMacw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/runtime-core": "3.5.13", - "@vue/shared": "3.5.13", + "@vue/reactivity": "3.5.14", + "@vue/runtime-core": "3.5.14", + "@vue/shared": "3.5.14", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", - "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.14.tgz", + "integrity": "sha512-Rf/ISLqokIvcySIYnv3tNWq40PLpNLDLSJwwVWzG6MNtyIhfbcrAxo5ZL9nARJhqjZyWWa40oRb2IDuejeuv6w==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-ssr": "3.5.14", + "@vue/shared": "3.5.14" }, "peerDependencies": { - "vue": "3.5.13" + "vue": "3.5.14" } }, "node_modules/@vue/shared": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", - "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.14.tgz", + "integrity": "sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==", "license": "MIT" }, "node_modules/@xmldom/xmldom": { @@ -3285,20 +3282,6 @@ "node": ">=6.5" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -3866,27 +3849,6 @@ "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", "license": "MIT" }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -4083,16 +4045,6 @@ "node": ">= 10.0.0" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/cacache": { "version": "16.1.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", @@ -4620,49 +4572,6 @@ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "license": "ISC" }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/copy-anything": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", @@ -4684,20 +4593,6 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "license": "MIT" }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/cose-base": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", @@ -4860,9 +4755,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4997,16 +4892,6 @@ "node": ">=0.10" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -5298,13 +5183,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -5322,9 +5200,9 @@ } }, "node_modules/electron": { - "version": "35.3.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-35.3.0.tgz", - "integrity": "sha512-6dLslJrQYB1qvqVPYRv1PhAA/uytC66nUeiTcq2JXiBzrmTWCHppqtGUjZhvnSRVatBCT5/SFdizdzcBiEiYUg==", + "version": "35.4.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-35.4.0.tgz", + "integrity": "sha512-VIPSNcUnic00aaE83w6BW4Dj1kE8A5DU0nVbvwqotN3+gseGunbP4WyHp/kfKXVKQj1S3No3HnYxU5LJmYbAtw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5489,112 +5367,6 @@ "node": ">= 10.0.0" } }, - "node_modules/electron-rebuild": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/electron-rebuild/-/electron-rebuild-3.2.9.tgz", - "integrity": "sha512-FkEZNFViUem3P0RLYbZkUjC8LUFIK+wKq09GHoOITSJjfDAVQv964hwaNseTTWt58sITQX3/5fHNYcTefqaCWw==", - "deprecated": "Please use @electron/rebuild moving forward. There is no API change, just a package name change", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", - "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "fs-extra": "^10.0.0", - "got": "^11.7.0", - "lzma-native": "^8.0.5", - "node-abi": "^3.0.0", - "node-api-version": "^0.1.4", - "node-gyp": "^9.0.0", - "ora": "^5.1.0", - "semver": "^7.3.5", - "tar": "^6.0.5", - "yargs": "^17.0.1" - }, - "bin": { - "electron-rebuild": "lib/src/cli.js" - }, - "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/electron-rebuild/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-rebuild/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-rebuild/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-rebuild/node_modules/node-api-version": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.1.4.tgz", - "integrity": "sha512-KGXihXdUChwJAOHO53bv9/vXcLmdUsZ6jIptbvYvkpKfth+r7jw44JkVxQFA3kX5nQjzjmGu1uAu/xNNLNlI5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/electron-rebuild/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-rebuild/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/electron-store": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-10.0.1.tgz", @@ -5719,16 +5491,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -5905,13 +5667,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -5926,9 +5681,9 @@ } }, "node_modules/eslint": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", - "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5936,14 +5691,13 @@ "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.26.0", - "@eslint/plugin-kit": "^0.2.8", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", - "@modelcontextprotocol/sdk": "^1.8.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -5967,8 +5721,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "zod": "^3.24.2" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -6004,6 +5757,22 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz", + "integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, "node_modules/eslint-plugin-vue": { "version": "9.33.0", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", @@ -6229,16 +5998,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -6248,29 +6007,6 @@ "node": ">=6" } }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", - "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/exceljs": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", @@ -6307,65 +6043,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, "node_modules/ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -6571,24 +6248,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6678,27 +6337,6 @@ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", "license": "MIT" }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/formdata-node": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", @@ -6712,26 +6350,6 @@ "node": ">= 12.20" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -7199,23 +6817,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -7451,16 +7052,6 @@ "node": ">= 12" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-ci": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", @@ -7566,13 +7157,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -7924,9 +7508,9 @@ } }, "node_modules/lightningcss": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", - "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -7939,22 +7523,22 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.29.2", - "lightningcss-darwin-x64": "1.29.2", - "lightningcss-freebsd-x64": "1.29.2", - "lightningcss-linux-arm-gnueabihf": "1.29.2", - "lightningcss-linux-arm64-gnu": "1.29.2", - "lightningcss-linux-arm64-musl": "1.29.2", - "lightningcss-linux-x64-gnu": "1.29.2", - "lightningcss-linux-x64-musl": "1.29.2", - "lightningcss-win32-arm64-msvc": "1.29.2", - "lightningcss-win32-x64-msvc": "1.29.2" + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", - "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", "cpu": [ "arm64" ], @@ -7972,9 +7556,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", - "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", "cpu": [ "x64" ], @@ -7992,9 +7576,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", - "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", "cpu": [ "x64" ], @@ -8012,9 +7596,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", - "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", "cpu": [ "arm" ], @@ -8032,9 +7616,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", - "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", "cpu": [ "arm64" ], @@ -8052,9 +7636,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", - "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", "cpu": [ "arm64" ], @@ -8072,9 +7656,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", - "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", "cpu": [ "x64" ], @@ -8092,9 +7676,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", - "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", "cpu": [ "x64" ], @@ -8112,9 +7696,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", - "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", "cpu": [ "arm64" ], @@ -8132,9 +7716,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", - "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", "cpu": [ "x64" ], @@ -8338,32 +7922,6 @@ "url": "https://github.com/sponsors/wellwelwel" } }, - "node_modules/lzma-native": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-8.0.6.tgz", - "integrity": "sha512-09xfg67mkL2Lz20PrrDeNYZxzeW7ADtpYFbwSQh9U8+76RIzx5QsJBMy8qikv3hbUPfpy6hqwxt6FcGK81g9AA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^3.1.0", - "node-gyp-build": "^4.2.1", - "readable-stream": "^3.6.0" - }, - "bin": { - "lzmajs": "bin/lzmajs" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/lzma-native/node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true, - "license": "MIT" - }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -8463,16 +8021,6 @@ "node": ">=12" } }, - "node_modules/make-fetch-happen/node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -8496,29 +8044,6 @@ "node": ">= 0.4" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8566,18 +8091,26 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" } }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -8881,9 +8414,9 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, "license": "MIT", "engines": { @@ -8959,141 +8492,6 @@ } } }, - "node_modules/node-gyp": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", - "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.0.3", - "nopt": "^6.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^12.13 || ^14.13 || >=16" - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "dev": true, - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-gyp/node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/node-gyp/node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^1.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -9166,19 +8564,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -9190,19 +8575,6 @@ "node": ">= 0.4" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -9229,9 +8601,9 @@ } }, "node_modules/openai": { - "version": "4.98.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.98.0.tgz", - "integrity": "sha512-TmDKur1WjxxMPQAtLG5sgBSCJmX7ynTsGmewKzoDwl1fRxtbLOsiR0FA/AOAAtYUmP6azal+MYQuOENfdU+7yg==", + "version": "4.100.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.100.0.tgz", + "integrity": "sha512-9soq/wukv3utxcuD7TWFqKdKp0INWdeyhUCvxwrne5KwnxaCp4eHL4GdT/tMFhYolxgNhxFzg5GFwM331Z5CZg==", "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", @@ -9399,21 +8771,10 @@ "node": ">=6" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, "license": "MIT" }, "node_modules/path-exists": { @@ -9479,16 +8840,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/pe-library": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", @@ -9557,16 +8908,6 @@ } } }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -9591,6 +8932,19 @@ "node": ">=4" } }, + "node_modules/portfinder": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz", + "integrity": "sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==", + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -9816,9 +9170,9 @@ } }, "node_modules/protobufjs": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.1.tgz", - "integrity": "sha512-3qx3IRjR9WPQKagdwrKjO3Gu8RgQR2qqw+1KnigWhoVjFqegIj1K3bP11sGqhxrO46/XL7lekuG4jmjL+4cLsw==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.2.tgz", + "integrity": "sha512-f2ls6rpO6G153Cy+o2XQ+Y0sARLOZ17+OGVLHrc3VUKcLHYKEKWbkSujdBWQXM7gKn5NTfp0XnRPZn1MIu8n9w==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -9839,20 +9193,6 @@ "node": ">=12.0.0" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/pug-error": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", @@ -9907,22 +9247,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9976,32 +9300,6 @@ "node": ">=0.12" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", @@ -10278,23 +9576,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10393,29 +9674,6 @@ "license": "MIT", "optional": true }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/seq-queue": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", @@ -10452,22 +9710,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -10480,13 +9722,6 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10510,82 +9745,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -10819,16 +9978,6 @@ "node": ">= 6" } }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -10951,9 +10100,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", - "integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz", + "integrity": "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==", "license": "MIT" }, "node_modules/tapable": { @@ -11266,16 +10415,6 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -11345,21 +10484,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -11428,16 +10552,6 @@ "node": ">= 4.0.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/unused-filename": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-4.0.1.tgz", @@ -11559,16 +10673,6 @@ "uuid": "dist/esm/bin/uuid" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -11718,16 +10822,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", - "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "version": "3.5.14", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.14.tgz", + "integrity": "sha512-LbOm50/vZFG6Mhy6KscQYXZMQ0LMCC/y40HDJPPvGFQ+i/lUH+PJHR6C3assgOQiXdl6tAfsXHbXYVBZZu65ew==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-sfc": "3.5.13", - "@vue/runtime-dom": "3.5.13", - "@vue/server-renderer": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.14", + "@vue/compiler-sfc": "3.5.14", + "@vue/runtime-dom": "3.5.14", + "@vue/server-renderer": "3.5.14", + "@vue/shared": "3.5.14" }, "peerDependencies": { "typescript": "*" @@ -12093,26 +11197,6 @@ "engines": { "node": ">= 10" } - }, - "node_modules/zod": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", - "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", - "devOptional": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } } } } diff --git a/package.json b/package.json index c77e6b1..6e01dd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "larabase", - "version": "1.0.0", + "version": "1.2.1", "main": "dist-electron/main/index.js", "description": "An Opined Database GUI for Laravel Developers", "author": "Tiago Padilha ", @@ -31,7 +31,9 @@ "publish:mac-intel": "vue-tsc --noEmit && vite build && electron-builder --publish always --mac --x64", "publish:mac-apple": "vue-tsc --noEmit && vite build && electron-builder --publish always --mac --arm64", "postinstall": "electron-builder install-app-deps", - "lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix", + "rebuild": "electron-builder install-app-deps", + "lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix --max-warnings 1000", + "lint:ci": "eslint . --ext .vue,.js,.ts,.jsx,.tsx --fix --max-warnings 1000 || true", "format": "prettier --write \"**/*.{js,ts,vue,jsx,tsx,css,md}\"" }, "dependencies": { @@ -53,10 +55,13 @@ "monaco-editor": "^0.52.2", "mysql2": "^3.14.1", "openai": "^4.98.0", + "path-browserify": "^1.0.1", "pinia": "^3.0.2", "pluralize": "^8.0.0", + "portfinder": "^1.0.37", "redis": "^5.0.0", "sql-formatter": "^15.6.1", + "ssh2": "^1.16.0", "tailwindcss": "^4.1.4", "uuid": "^11.1.0", "vue-router": "^4.5.1" @@ -66,7 +71,9 @@ "@types/bcrypt": "^5.0.2", "@types/dockerode": "^3.3.38", "@types/lodash": "^4.17.16", + "@types/path-browserify": "^1.0.3", "@types/pluralize": "^0.0.33", + "@types/ssh2": "^1.15.0", "@typescript-eslint/eslint-plugin": "^8.31.1", "@typescript-eslint/parser": "^8.31.1", "@vitejs/plugin-vue": "^5.0.4", @@ -75,6 +82,7 @@ "electron-builder": "^26.0.14", "eslint": "^9.25.1", "eslint-config-prettier": "^10.1.2", + "eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-vue": "^9.33.0", "eslint-plugin-vue-pug": "^0.6.2", "globals": "^16.1.0", @@ -90,8 +98,8 @@ "vue-tsc": "^2.2.10" }, "optionalDependencies": { - "@tailwindcss/oxide-darwin-x64": "^4.1.7", - "@tailwindcss/oxide-darwin-arm64": "^4.1.7" + "@tailwindcss/oxide-darwin-arm64": "^4.1.7", + "@tailwindcss/oxide-darwin-x64": "^4.1.7" }, "build": { "appId": "com.larabase.app", @@ -134,7 +142,9 @@ "target": [ { "target": "nsis", - "arch": ["x64"] + "arch": [ + "x64" + ] } ], "icon": "dist/icons/win/icon.ico" @@ -147,7 +157,13 @@ "icon": "dist/icons/png/512x512.png" }, "asarUnpack": [ - "node_modules/electron-store/**/*" + "node_modules/electron-store/**/*", + "node_modules/ssh2/**/*", + "node_modules/bcrypt/**/*", + "node_modules/mysql2/**/*", + "node_modules/dockerode/**/*", + "node_modules/ioredis/**/*", + "node_modules/**/build/Release/*.node" ] } -} \ No newline at end of file +} diff --git a/src/components/BaseHeader.vue b/src/components/BaseHeader.vue index c360adb..63293df 100644 --- a/src/components/BaseHeader.vue +++ b/src/components/BaseHeader.vue @@ -1,5 +1,9 @@ diff --git a/src/components/Migrations.vue b/src/components/Migrations.vue index aaa6499..526b324 100644 --- a/src/components/Migrations.vue +++ b/src/components/Migrations.vue @@ -4,6 +4,7 @@ import Modal from '@/components/Modal.vue'; import PhpViewer from '@/components/PhpViewer.vue'; import { useConnectionsStore } from '@/store/connections'; import terminalService from '@/services/terminal'; +import { AppConnection } from '@/types/ssh-connection'; const emit = defineEmits(['close', 'migrations-updated']); const connectionsStore = useConnectionsStore(); @@ -183,10 +184,17 @@ onMounted(async () => { } try { + const project = selectedProject.value; + + const AppConnection = { + localDbConfig: toRaw(project.dbConfig), + remote: toRaw(project.sshConfig) + } as AppConnection; + const config = { - projectPath: selectedProject.value.projectPath, - usingSail: selectedProject.value.usingSail, - db_config: toRaw(selectedProject.value.db_config) + projectPath: project.projectPath, + usingSail: project.usingSail, + appConnection: AppConnection }; const result = await window.ipcRenderer.invoke( diff --git a/src/components/Modal.vue b/src/components/Modal.vue index 799d829..e95bb71 100644 --- a/src/components/Modal.vue +++ b/src/components/Modal.vue @@ -6,7 +6,8 @@ > - - - - - - +
+
+
+ Database: + {{ databaseName }} +
+ +
+
- - diff --git a/src/views/Home.vue b/src/views/Home.vue index 6b7e125..250976b 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -7,6 +7,8 @@ import RestoreDatabase from '@/components/home/RestoreDatabase.vue'; import { ProjectConnection } from '@/types/project'; import ManageConnection from '@/components/home/ManageConnection.vue'; import Settings from '@/components/Settings.vue'; +import { ConnectionType } from '@/types/connection-types'; +import RemoteBadge from '@/components/ui/RemoteBadge.vue'; const router = useRouter(); const connectionsStore = useConnectionsStore(); @@ -48,16 +50,22 @@ onMounted(async () => { await connectionsStore.loadConnections(); }); -function openConnection(connectionId: string) { - router.push(`/database/${connectionId}`); +async function openConnection(project: ProjectConnection) { + const success = await window.ipcRenderer.openConnectionWindow( + project.id, + project.isRemote + ); + if (success) { + window.close(); + } } function getConnectionColor(type: string) { switch (type) { - case 'mysql': + case ConnectionType.MySQL: return 'bg-orange-500'; - case 'postgresql': - return 'bg-blue-600'; + case ConnectionType.SSH: + return 'bg-purple-600'; default: return 'bg-gray-600'; } @@ -172,7 +180,7 @@ function getConnectionColor(type: string) {
{{ connection.name }} +

- {{ connection.db_config.host }} + {{ + connection.type === + ConnectionType.SSH && + connection.sshConfig + ? connection.sshConfig.host + : connection.dbConfig?.host + }}

- {{ connection.db_config.database }} + {{ + connection.type === + ConnectionType.SSH && + connection.sshConfig + ? connection.sshConfig + .remoteDbConfig.database + : connection.dbConfig?.database + }} {{ connection.status }}

@@ -305,9 +337,7 @@ function getConnectionColor(type: string) { :disabled="!connection.isValid" class="btn btn-sm btn-ghost" @click.stop=" - openConnection( - connection.id as string - ) + openConnection(connection) " > ({ isExplaining: false }); const isProcessing = ref(false); +const isRemoteConnection = computed(() => { + return route.params.isRemote === 'true'; +}); const currentPage = ref(1); const rowsPerPage = ref(25); @@ -148,10 +150,6 @@ const tableData = computed(() => { }); }); -function goBack() { - router.push(`/database/${projectId.value}`); -} - function loadSavedSql() { const savedSql = localStorage.getItem( `sql-editor-content-${projectId.value}` @@ -355,9 +353,16 @@ function handleConnectionValid() { onMounted(async () => { isLoading.value = true; + await connectionsStore.loadConnections(projectId.value); + sqlScratchService.loadScratches(projectId.value); - await initializeSchema(); + + if (!isRemoteConnection.value) { + await initializeSchema(); + } else { + isContentReady.value = true; + } const active = sqlScratchService.getActiveScratch(); if (active) { @@ -460,6 +465,7 @@ onBeforeUnmount(() => {
{