feat: implement CLI environment management services and add internationalization support

This commit is contained in:
Ümit Tunç
2026-04-02 10:57:11 +03:00
parent 3627feac1c
commit ab23b25a17
7 changed files with 321 additions and 6 deletions
+29
View File
@@ -13,6 +13,7 @@ import { portMappingService } from '../services/PortMappingService'
import path from 'path' import path from 'path'
import { mariaDbOperationService } from '../services/MariaDbOperationService' import { mariaDbOperationService } from '../services/MariaDbOperationService'
import { cliAliasService } from '../services/CliAliasService' import { cliAliasService } from '../services/CliAliasService'
import { cliBinService } from '../services/CliBinService'
function findExecutable(dir: string, name: string): string | null { function findExecutable(dir: string, name: string): string | null {
if (!fs.existsSync(dir)) return null if (!fs.existsSync(dir)) return null
@@ -1041,4 +1042,32 @@ export function registerIpcHandlers(): void {
rootPass: rootPass || '' rootPass: rootPass || ''
}, dbName, savePath, compress, (msg) => log(msg)) }, dbName, savePath, compress, (msg) => log(msg))
}) })
// CLI & Environment
ipcMain.handle('cli:get-config', async () => {
const settings = configService.getSettings()
return {
enabled: settings.cliEnvEnabled !== false,
defaultPhp: settings.defaultPhpVersion,
defaultMariaDb: settings.defaultMariaDbVersion,
binDir: cliBinService.getBinDir()
}
})
ipcMain.handle('cli:set-config', async (_event, config: any) => {
const settings = configService.getSettings()
configService.updateSettings({
...settings,
cliEnvEnabled: config.enabled,
defaultPhpVersion: config.defaultPhp,
defaultMariaDbVersion: config.defaultMariaDb
})
await cliAliasService.generateAliases()
return { success: true }
})
ipcMain.handle('cli:sync', async () => {
await cliAliasService.generateAliases()
return { success: true }
})
} }
+2
View File
@@ -2,6 +2,7 @@ import fs from 'fs'
import { phpManagerService } from './PhpManagerService' import { phpManagerService } from './PhpManagerService'
import { mariaDbManagerService } from './MariaDbManagerService' import { mariaDbManagerService } from './MariaDbManagerService'
import { configService } from './ConfigService' import { configService } from './ConfigService'
import { cliBinService } from './CliBinService'
export class CliAliasService { export class CliAliasService {
async generateAliases() { async generateAliases() {
@@ -13,6 +14,7 @@ export class CliAliasService {
await this.generatePowerShellAliases(installedPhp, installedMariaDb) await this.generatePowerShellAliases(installedPhp, installedMariaDb)
await this.generateBashAliases(installedPhp, installedMariaDb) await this.generateBashAliases(installedPhp, installedMariaDb)
await cliBinService.sync()
} }
private async generatePowerShellAliases(phpVersions: any[], mariaDbVersions: any[]) { private async generatePowerShellAliases(phpVersions: any[], mariaDbVersions: any[]) {
+146
View File
@@ -0,0 +1,146 @@
import fs from 'fs'
import path from 'path'
import { app } from 'electron'
import { exec } from 'child_process'
import { promisify } from 'util'
import { phpManagerService } from './PhpManagerService'
import { mariaDbManagerService } from './MariaDbManagerService'
import { configService } from './ConfigService'
const execAsync = promisify(exec)
export class CliBinService {
private binDir: string
constructor() {
// We store shims in the app's roaming/user path to avoid permission issues
this.binDir = path.join(app.getPath('userData'), 'cli-bin')
if (!fs.existsSync(this.binDir)) {
fs.mkdirSync(this.binDir, { recursive: true })
}
}
async sync() {
const phpVersions = await phpManagerService.getVersions()
const mariaDbVersions = await mariaDbManagerService.getVersions()
const settings = configService.getSettings()
const installedPhp = phpVersions.filter(v => v.status === 'installed')
const installedMariaDb = mariaDbVersions.filter(v => v.status === 'installed')
// Clean current shims
const files = fs.readdirSync(this.binDir)
for (const file of files) {
fs.unlinkSync(path.join(this.binDir, file))
}
// Generate PHP shims
for (const v of installedPhp) {
const binaryPath = await phpManagerService.getPhpBinaryPath(v.version)
if (binaryPath) {
// version: 8.1.2 -> php812, php81, php8, php8.1
const vParts = v.version.split('.')
const aliases = [
`php${v.version.replace(/\./g, '')}`, // php812
`php${vParts[0]}${vParts[1]}`, // php81
`php${vParts[0]}.${vParts[1]}` // php8.1
]
for (const alias of aliases) {
this.createWindowsShim(alias, binaryPath)
}
// If this is the chosen default binary for general 'php'
if (settings.defaultPhpVersion === v.version) {
this.createWindowsShim('php', binaryPath)
}
}
}
// If no default is set, use the highest version as generic 'php'
if (!settings.defaultPhpVersion && installedPhp.length > 0) {
const latest = installedPhp[0]
const path = await phpManagerService.getPhpBinaryPath(latest.version)
if (path) this.createWindowsShim('php', path)
}
// Generate MariaDB shims
for (const v of installedMariaDb) {
const binaryPath = await mariaDbManagerService.getMariaDbBinaryPath(v.version)
if (binaryPath) {
const vParts = v.version.split('.')
const aliases = [
`mysql${vParts[0]}${vParts[1]}`,
`mariadb${vParts[0]}${vParts[1]}`,
`mysql${vParts[0]}.${vParts[1]}`,
`mariadb${vParts[0]}.${vParts[1]}`
]
for (const alias of aliases) {
this.createWindowsShim(alias, binaryPath)
}
if (settings.defaultMariaDbVersion === v.id) {
this.createWindowsShim('mysql', binaryPath)
this.createWindowsShim('mariadb', binaryPath)
}
}
}
// Default mysql/mariadb if none selected
if (!settings.defaultMariaDbVersion && installedMariaDb.length > 0) {
const latest = installedMariaDb[0]
const path = await mariaDbManagerService.getMariaDbBinaryPath(latest.version)
if (path) {
this.createWindowsShim('mysql', path)
this.createWindowsShim('mariadb', path)
}
}
// Always ensure bin folder is in User PATH if enabled
if (settings.cliEnvEnabled !== false) {
await this.ensureInPath()
}
}
private createWindowsShim(name: string, targetPath: string) {
if (process.platform !== 'win32') {
// For Unix we could use symlinks, but Multi-PHP target is mainly Windows for now
// Symlinks are also fine: fs.symlinkSync(targetPath, path.join(this.binDir, name))
return
}
// Create a small .cmd proxy for each command
// Using "@%* " to pass all arguments correctly
const content = `@echo off\n"${targetPath}" %*\n`
fs.writeFileSync(path.join(this.binDir, `${name}.cmd`), content)
// Also create a .bat just in case
fs.writeFileSync(path.join(this.binDir, `${name}.bat`), content)
}
async ensureInPath() {
if (process.platform !== 'win32') return
try {
// Get current User PATH
const { stdout } = await execAsync('powershell -Command "[Environment]::GetEnvironmentVariable(\'Path\', \'User\')"')
const currentPath = stdout.trim()
if (!currentPath.includes(this.binDir)) {
const newPath = `${currentPath}${currentPath.endsWith(';') ? '' : ';'}${this.binDir}`
// Update User PATH safely using PowerShell
await execAsync(`powershell -Command "[Environment]::SetEnvironmentVariable('Path', '${newPath}', 'User')"`)
console.log(`Added Multi-PHP bin to User PATH: ${this.binDir}`)
}
} catch (e) {
console.error('Failed to update PATH:', e)
}
}
getBinDir() {
return this.binDir
}
}
export const cliBinService = new CliBinService()
+4 -1
View File
@@ -14,7 +14,10 @@ export class ConfigService {
mariaDbPort: '3306', mariaDbPort: '3306',
mariaDbPorts: {} as Record<string, string>, mariaDbPorts: {} as Record<string, string>,
allowRemoteAccess: false, allowRemoteAccess: false,
language: 'tr' language: 'tr',
cliEnvEnabled: true,
defaultPhpVersion: '',
defaultMariaDbVersion: ''
} }
constructor() { constructor() {
+112 -1
View File
@@ -119,6 +119,7 @@ function App(): JSX.Element {
}) })
const [activeTab, setActiveTab] = useState('dashboard') const [activeTab, setActiveTab] = useState('dashboard')
const [projectSearch, setProjectSearch] = useState('') const [projectSearch, setProjectSearch] = useState('')
const [cliConfig, setCliConfig] = useState<any>({ enabled: true, defaultPhp: '', defaultMariaDb: '', binDir: '' })
const [settingsTab, setSettingsTab] = useState(0) const [settingsTab, setSettingsTab] = useState(0)
const [dashboardTab, setDashboardTab] = useState(0) const [dashboardTab, setDashboardTab] = useState(0)
const [projects, setProjects] = useState<any[]>([]) const [projects, setProjects] = useState<any[]>([])
@@ -188,6 +189,13 @@ function App(): JSX.Element {
} }
} }
const fetchCliConfig = async () => {
if (window.api && window.api.invoke) {
const config = await window.api.invoke('cli:get-config')
if (config) setCliConfig(config)
}
}
const fetchProjects = async () => { const fetchProjects = async () => {
if (window.api && window.api.getProjects) { if (window.api && window.api.getProjects) {
const list = await window.api.getProjects() const list = await window.api.getProjects()
@@ -264,6 +272,7 @@ function App(): JSX.Element {
fetchNginxVersions() fetchNginxVersions()
fetchApacheVersions() fetchApacheVersions()
fetchSettings() fetchSettings()
fetchCliConfig()
fetchPmaStatus() fetchPmaStatus()
const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => { const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => {
@@ -975,7 +984,8 @@ function App(): JSX.Element {
<Tab label="MariaDB" icon={<DbIcon />} iconPosition="start" /> <Tab label="MariaDB" icon={<DbIcon />} iconPosition="start" />
<Tab label="Nginx" icon={<WebIcon />} iconPosition="start" /> <Tab label="Nginx" icon={<WebIcon />} iconPosition="start" />
<Tab label="Apache" icon={<ServerIcon />} iconPosition="start" /> <Tab label="Apache" icon={<ServerIcon />} iconPosition="start" />
<Tab label={t('settings.server_settings')} icon={<ServerIcon />} iconPosition="start" /> <Tab label={t('settings.server_settings')} icon={<SettingsIcon />} iconPosition="start" />
<Tab label={t('settings.cli_env')} icon={<TerminalIcon />} iconPosition="start" />
</Tabs> </Tabs>
</Box> </Box>
@@ -1215,6 +1225,107 @@ function App(): JSX.Element {
</Grid> </Grid>
</Paper> </Paper>
)} )}
{settingsTab === 5 && (
<Box>
<Paper sx={{ p: 3, mb: 3, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1, gap: 1 }}>
<TerminalIcon color="primary" />
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>{t('settings.cli_env')}</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t('settings.cli_env_desc')}
</Typography>
<Stack spacing={3}>
<Box>
<FormControlLabel
control={
<Switch
checked={cliConfig.enabled}
onChange={async (e) => {
const newConf = { ...cliConfig, enabled: e.target.checked }
setCliConfig(newConf)
await window.api.invoke('cli:set-config', newConf)
}}
/>
}
label={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>{t('settings.cli_enabled')}</Typography>
<Typography variant="caption" color="text.secondary" display="block">{t('settings.cli_enabled_desc')}</Typography>
</Box>
}
/>
</Box>
<Divider />
<Box sx={{ display: 'flex', gap: 3, flexDirection: { xs: 'column', md: 'row' } }}>
<FormControl fullWidth size="small">
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>{t('settings.cli_default_php')}</Typography>
<Select
value={cliConfig.defaultPhp}
onChange={async (e) => {
const newConf = { ...cliConfig, defaultPhp: e.target.value }
setCliConfig(newConf)
await window.api.invoke('cli:set-config', newConf)
}}
>
<MenuItem value=""><em>{t('common.ready')}</em></MenuItem>
{phpVersions.filter(v => v.status === 'installed').map(v => (
<MenuItem key={v.id} value={v.version}>PHP {v.version}</MenuItem>
))}
</Select>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5 }}>{t('settings.cli_default_php_desc')}</Typography>
</FormControl>
<FormControl fullWidth size="small">
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>{t('settings.cli_default_mariadb')}</Typography>
<Select
value={cliConfig.defaultMariaDb}
onChange={async (e) => {
const newConf = { ...cliConfig, defaultMariaDb: e.target.value }
setCliConfig(newConf)
await window.api.invoke('cli:set-config', newConf)
}}
>
<MenuItem value=""><em>{t('common.ready')}</em></MenuItem>
{mariaDbVersions.filter(v => v.status === 'installed').map(v => (
<MenuItem key={v.id} value={v.id}>MariaDB {v.version}</MenuItem>
))}
</Select>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5 }}>{t('settings.cli_default_mariadb_desc')}</Typography>
</FormControl>
</Box>
<Divider />
<Box sx={{ p: 2, bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.4)', textTransform: 'uppercase', fontWeight: 800, mb: 0.5, display: 'block' }}>
{cliConfig.enabled ? t('settings.cli_path_found') : t('settings.cli_path_not_found')}
</Typography>
<Typography variant="body2" sx={{ fontFamily: 'monospace', color: cliConfig.enabled ? '#33d9b2' : 'error.main' }}>
{cliConfig.binDir || '...'}
</Typography>
</Box>
<Button
variant="outlined"
size="small"
startIcon={<RefreshIcon />}
onClick={async () => {
await window.api.invoke('cli:sync')
setNotification({ open: true, message: t('settings.cli_sync_success'), severity: 'success' })
}}
>
{t('settings.cli_sync_btn')}
</Button>
</Box>
</Stack>
</Paper>
</Box>
)}
</Box> </Box>
)} )}
+15 -3
View File
@@ -99,9 +99,21 @@
"php_versions_desc": "Download and manage required PHP versions here.", "php_versions_desc": "Download and manage required PHP versions here.",
"mariadb_versions_desc": "Manage MariaDB versions here.", "mariadb_versions_desc": "Manage MariaDB versions here.",
"nginx_versions_desc": "Manage Nginx versions here.", "nginx_versions_desc": "Manage Nginx versions here.",
"apache_versions_desc": "Manage Apache versions here.", "apache_versions_desc": "You can manage Apache versions from here.",
"server_settings": "Server Settings", "nginx_port_desc": "Port on which Nginx runs (Default: 80)",
"nginx_port_desc": "Port Nginx will run on (Default: 80)" "cli_env": "CLI & Environment Variables",
"cli_env_desc": "Manage PATH settings to run PHP and MariaDB commands directly from any terminal (CMD, PowerShell).",
"cli_enabled": "System PATH Integration Active",
"cli_enabled_desc": "When ON, the Multi-PHP 'bin' folder is added to your user PATH variable.",
"cli_default_php": "Default PHP Version (php)",
"cli_default_php_desc": "Choose which version will be used when you run the 'php' command.",
"cli_default_mariadb": "Default MariaDB Version (mysql)",
"cli_default_mariadb_desc": "Default version for 'mysql' or 'mariadb' commands.",
"cli_sync_btn": "SYNC SETTINGS",
"cli_path_found": "PATH Found:",
"cli_path_not_found": "PATH Not Set Yet",
"cli_sync_success": "CLI settings and PATH integration updated successfully.",
"server_settings": "Server Settings"
}, },
"server": { "server": {
"nginx": { "nginx": {
+13 -1
View File
@@ -101,7 +101,19 @@
"mariadb_versions_desc": "MariaDB sürümlerini buradan yönetebilirsiniz.", "mariadb_versions_desc": "MariaDB sürümlerini buradan yönetebilirsiniz.",
"nginx_versions_desc": "Nginx sürümlerini buradan yönetebilirsiniz.", "nginx_versions_desc": "Nginx sürümlerini buradan yönetebilirsiniz.",
"apache_versions_desc": "Apache sürümlerini buradan yönetebilirsiniz.", "apache_versions_desc": "Apache sürümlerini buradan yönetebilirsiniz.",
"nginx_port_desc": "Nginx'in çalışacağı port (Varsayılan: 80)" "nginx_port_desc": "Nginx'in çalışacağı port (Varsayılan: 80)",
"cli_env": "CLI & Ortam Değişkenleri",
"cli_env_desc": "PHP ve MariaDB komutlarını herhangi bir terminalden (CMD, PowerShell) doğrudan çalıştırabilmeniz için PATH ayarlarını yönetin.",
"cli_enabled": "Sistem PATH Entegrasyonu Aktif",
"cli_enabled_desc": "Açık olduğunda, Multi-PHP 'bin' klasörü sisteminizin kullanıcı PATH değişkenine eklenir.",
"cli_default_php": "Varsayılan PHP Sürümü (php)",
"cli_default_php_desc": "'php' komutunu çalıştırdığınızda hangi sürümün kullanılacağını seçin.",
"cli_default_mariadb": "Varsayılan MariaDB Sürümü (mysql)",
"cli_default_mariadb_desc": "'mysql' veya 'mariadb' komutu için varsayılan sürüm.",
"cli_sync_btn": "AYARLARI SENKRONİZE ET",
"cli_path_found": "PATH Ayarı Bulundu:",
"cli_path_not_found": "PATH Ayarı Henüz Yapılmamış",
"cli_sync_success": "CLI ayarları ve PATH entegrasyonu başarıyla güncellendi."
}, },
"server": { "server": {
"nginx": { "nginx": {