diff --git a/src/handlers/installationHandlers.js b/src/handlers/installationHandlers.js index 6616416..9141bc0 100644 --- a/src/handlers/installationHandlers.js +++ b/src/handlers/installationHandlers.js @@ -1,11 +1,15 @@ -import { createSpinner } from 'nanospinner'; -import { runCommand } from '../utils/command.js'; -import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js'; -import { checkDependencies, isCodexInstalled } from '../services/nodeService.js'; +import path from 'path'; import inquirer from 'inquirer'; import boxen from 'boxen'; import chalk from 'chalk'; import os from 'os'; +import fs from 'fs'; +import { createSpinner } from 'nanospinner'; +import { runCommand } from '../utils/command.js'; +import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js'; +import { checkDependencies } from '../services/nodeService.js'; +import { saveConfig } from '../services/config.js'; +import { getCodexRootPath, getCodexBinPath, getCodexDataDirDefaultPath, getCodexLogsPath } from '../utils/appdata.js'; const platform = os.platform(); @@ -52,19 +56,54 @@ These information will be used for calculating various metrics that can eventual return agreement.toLowerCase() === 'y'; } -export async function checkCodexInstallation(showNavigationMenu) { +export async function getCodexVersion(config) { + if (config.codexExe.length < 1) return ""; + try { - const version = await runCommand('codex --version'); - console.log(chalk.green('Codex is already installed. Version:')); - console.log(chalk.green(version)); - await showNavigationMenu(); + const version = await runCommand(`"${config.codexExe}" --version`); + if (version.length < 1) throw new Error("Version info not found."); + return version; } catch (error) { - console.log(chalk.cyanBright('Codex is not installed, proceeding with installation...')); - await installCodex(showNavigationMenu); + return ""; } } -export async function installCodex(showNavigationMenu) { +export async function checkCodexInstallation(config, showNavigationMenu) { + const version = await getCodexVersion(config); + + if (version.length > 0) { + console.log(chalk.green('Codex is already installed. Version:')); + console.log(chalk.green(version)); + await showNavigationMenu(); + } else { + console.log(chalk.cyanBright('Codex is not installed, proceeding with installation...')); + await installCodex(config, showNavigationMenu); + } +} + +async function saveCodexExePathToConfig(config, codexExePath) { + config.codexExe = codexExePath; + config.dataDir = getCodexDataDirDefaultPath(); + config.logsDir = getCodexLogsPath(); + if (!fs.existsSync(config.codexExe)) { + console.log(showErrorMessage(`Codex executable not found in expected path: ${config.codexExe}`)); + throw new Error("Exe not found"); + } + if (await getCodexVersion(config).length < 1) { + console.log(showInfoMessage("no")); + throw new Error(`Codex not found at path after install. Path: '${config.codexExe}'`); + } + saveConfig(config); +} + +async function clearCodexExePathFromConfig(config) { + config.codexExe = ""; + config.dataDir = ""; + config.logsDir = ""; + saveConfig(config); +} + +export async function installCodex(config, showNavigationMenu) { const agreed = await showPrivacyDisclaimer(); if (!agreed) { console.log(showInfoMessage('You can find manual setup instructions at docs.codex.storage')); @@ -72,40 +111,25 @@ export async function installCodex(showNavigationMenu) { return; } + const installPath = getCodexBinPath(); + console.log(showInfoMessage("Install location: " + installPath)); + + const spinner = createSpinner('Installing Codex...').start(); + try { - const spinner = createSpinner('Downloading Codex binaries...').start(); if (platform === 'win32') { try { try { await runCommand('curl --version'); } catch (error) { - spinner.error(); throw new Error('curl is not available. Please install curl or update your Windows version.'); } await runCommand('curl -LO --ssl-no-revoke https://get.codex.storage/install.cmd'); + await runCommand(`set "INSTALL_DIR=${installPath}" && "${process.cwd()}\\install.cmd"`); - const currentDir = process.cwd(); - await runCommand(`"${currentDir}\\install.cmd"`); - - await runCommand('set "PATH=%PATH%;%LOCALAPPDATA%\\Codex"'); - - try { - await runCommand('setx PATH "%PATH%;%LOCALAPPDATA%\\Codex"'); - spinner.success(); - console.log(showSuccessMessage('Codex has been installed and PATH has been updated automatically!\n' + - `You may need to restart your terminal.` - )); - } catch (error) { - spinner.success(); - console.log(showInfoMessage( - 'To complete installation:\n\n' + - '1. Open Control Panel → System → Advanced System settings → Environment Variables\n' + - '2. Or type "environment variables" in Windows Search\n' + - '3. Add "%LOCALAPPDATA%\\Codex" to your Path variable' - )); - } + await saveCodexExePathToConfig(config, path.join(installPath, "codex.exe")); try { await runCommand('del /f install.cmd'); @@ -113,23 +137,20 @@ export async function installCodex(showNavigationMenu) { // Ignore cleanup errors } } catch (error) { - spinner.error(); if (error.message.includes('Access is denied')) { throw new Error('Installation failed. Please run the command prompt as Administrator and try again.'); } else if (error.message.includes('curl')) { throw new Error(error.message); } else { - throw new Error('Installation failed. Please check your internet connection and try again.'); + throw new Error(`Installation failed: "${error.message}"`); } } } else { try { const dependenciesInstalled = await checkDependencies(); if (!dependenciesInstalled) { - spinner.error(); console.log(showInfoMessage('Please install the required dependencies and try again.')); - await showNavigationMenu(); - return; + throw new Error("Missing dependencies."); } const downloadCommand = 'curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh'; @@ -140,19 +161,19 @@ export async function installCodex(showNavigationMenu) { eval { local $SIG{ALRM} = sub { die "timeout\\n" }; alarm(120); - system("bash install.sh"); + system("INSTALL_DIR=\\"${installPath}\\" bash install.sh"); alarm(0); }; die if $@; '`; await runCommand(timeoutCommand); } else { - await runCommand('timeout 120 bash install.sh'); + await runCommand(`INSTALL_DIR="${installPath}" timeout 120 bash install.sh`); } + + await saveCodexExePathToConfig(config, path.join(installPath, "codex")); - spinner.success(); } catch (error) { - spinner.error(); if (error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')) { throw new Error('Installation failed. Please check your internet connection and try again.'); } else if (error.message.includes('Permission denied')) { @@ -168,28 +189,38 @@ export async function installCodex(showNavigationMenu) { } try { - const version = await runCommand('codex --version'); + const version = await getCodexVersion(config); console.log(showSuccessMessage( - 'Codex is successfully installed!\n\n' + + 'Codex is successfully installed!\n' + + `Install path: "${config.codexExe}"\n\n` + `Version: ${version}` )); } catch (error) { throw new Error('Installation completed but Codex command is not available. Please restart your terminal and try again.'); } + spinner.success(); await showNavigationMenu(); } catch (error) { + spinner.error(); console.log(showErrorMessage(`Failed to install Codex: ${error.message}`)); await showNavigationMenu(); } } -export async function uninstallCodex(showNavigationMenu) { +function removeDir(dir) { + fs.rmSync(dir, { recursive: true, force: true }); +} + +export async function uninstallCodex(config, showNavigationMenu) { const { confirm } = await inquirer.prompt([ { type: 'confirm', name: 'confirm', - message: chalk.yellow('⚠️ Are you sure you want to uninstall Codex? This action cannot be undone.'), + message: chalk.yellow( + '⚠️ Are you sure you want to uninstall Codex? This action cannot be undone. \n' + + 'All data stored in the local Codex node will be deleted as well.' + ), default: false } ]); @@ -201,26 +232,9 @@ export async function uninstallCodex(showNavigationMenu) { } try { - if (platform === 'win32') { - console.log(showInfoMessage('Removing Codex from Windows...')); - - await runCommand('netsh advfirewall firewall delete rule name="Allow Codex (TCP-In)"'); - await runCommand('netsh advfirewall firewall delete rule name="Allow Codex (UDP-In)"'); - - await runCommand('rd /s /q "%LOCALAPPDATA%\\Codex"'); - - console.log(showInfoMessage( - 'To complete uninstallation:\n\n' + - '1. Open Control Panel → System → Advanced System settings → Environment Variables\n' + - '2. Or type "environment variables" in Windows Search\n' + - '3. Remove "%LOCALAPPDATA%\\Codex" from your Path variable' - )); - } else { - const binaryPath = '/usr/local/bin/codex'; - console.log(showInfoMessage(`Attempting to remove Codex binary from ${binaryPath}...`)); - await runCommand(`sudo rm ${binaryPath}`); - } - + removeDir(getCodexRootPath()); + clearCodexExePathFromConfig(config); + console.log(showSuccessMessage('Codex has been successfully uninstalled.')); await showNavigationMenu(); } catch (error) { @@ -231,4 +245,4 @@ export async function uninstallCodex(showNavigationMenu) { } await showNavigationMenu(); } -} \ No newline at end of file +} diff --git a/src/handlers/nodeHandlers.js b/src/handlers/nodeHandlers.js index 5988935..b6dc09e 100644 --- a/src/handlers/nodeHandlers.js +++ b/src/handlers/nodeHandlers.js @@ -1,7 +1,8 @@ +import path from 'path'; import { createSpinner } from 'nanospinner'; import { runCommand } from '../utils/command.js'; import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js'; -import { isNodeRunning, isCodexInstalled, logToSupabase, startPeriodicLogging, getWalletAddress, setWalletAddress } from '../services/nodeService.js'; +import { isNodeRunning, isCodexInstalled, startPeriodicLogging, getWalletAddress, setWalletAddress } from '../services/nodeService.js'; import inquirer from 'inquirer'; import boxen from 'boxen'; import chalk from 'chalk'; @@ -27,8 +28,15 @@ async function promptForWalletAddress() { return wallet || null; } -export async function runCodex(showNavigationMenu) { - const isInstalled = await isCodexInstalled(); +function getCurrentLogFile(config) { + const timestamp = new Date().toISOString() + .replaceAll(":", "-") + .replaceAll(".", "-"); + return path.join(config.logsDir, `codex_${timestamp}.log`); +} + +export async function runCodex(config, showNavigationMenu) { + const isInstalled = await isCodexInstalled(config); if (!isInstalled) { console.log(showErrorMessage('Codex is not installed. Please install Codex first using option 1 from the main menu.')); await showNavigationMenu(); @@ -65,9 +73,19 @@ export async function runCodex(showNavigationMenu) { nat = await runCommand('curl -s https://ip.codex.storage'); } - const executable = `codex`; + if (config.dataDir.length < 1) throw new Error("Missing config: dataDir"); + if (config.logsDir.length < 1) throw new Error("Missing config: logsDir"); + const logFilePath = getCurrentLogFile(config); + + console.log(showInfoMessage( + `Data location: ${config.dataDir}\n` + + `Logs: ${logFilePath}` + )); + + const executable = config.codexExe; const args = [ - `--data-dir=datadir`, + `--data-dir="${config.dataDir}"`, + `--log-file="${logFilePath}"`, `--disc-port=${discPort}`, `--listen-addrs=/ip4/0.0.0.0/tcp/${listenPort}`, `--nat=${nat}`, @@ -76,10 +94,11 @@ export async function runCodex(showNavigationMenu) { ]; const command = - `${executable} ${args.join(" ")}` + `"${executable}" ${args.join(" ")}` console.log(showInfoMessage( '🚀 Codex node is running...\n\n' + + 'If your firewall ask, be sure to allow Codex to receive connections. \n' + 'Please keep this terminal open. Start a new terminal to interact with the node.\n\n' + 'Press CTRL+C to stop the node' )); diff --git a/src/main.js b/src/main.js index c93c116..9997f79 100644 --- a/src/main.js +++ b/src/main.js @@ -9,6 +9,7 @@ import { uploadFile, downloadFile, showLocalFiles } from './handlers/fileHandler import { checkCodexInstallation, installCodex, uninstallCodex } from './handlers/installationHandlers.js'; import { runCodex, checkNodeStatus } from './handlers/nodeHandlers.js'; import { showInfoMessage } from './utils/messages.js'; +import { loadConfig } from './services/config.js'; async function showNavigationMenu() { console.log('\n') @@ -67,9 +68,9 @@ export async function main() { process.on('SIGQUIT', handleExit); try { + const config = loadConfig(); while (true) { console.log('\n' + chalk.cyanBright(ASCII_ART)); - const { choice } = await inquirer.prompt([ { type: 'list', @@ -101,10 +102,10 @@ export async function main() { switch (choice.split('.')[0]) { case '1': - await checkCodexInstallation(showNavigationMenu); + await checkCodexInstallation(config, showNavigationMenu); break; case '2': - await runCodex(showNavigationMenu); + await runCodex(config, showNavigationMenu); return; case '3': await checkNodeStatus(showNavigationMenu); @@ -119,7 +120,7 @@ export async function main() { await showLocalFiles(showNavigationMenu); break; case '7': - await uninstallCodex(showNavigationMenu); + await uninstallCodex(config, showNavigationMenu); break; case '8': const { exec } = await import('child_process'); diff --git a/src/services/config.js b/src/services/config.js new file mode 100644 index 0000000..7bc2f4a --- /dev/null +++ b/src/services/config.js @@ -0,0 +1,46 @@ +import fs from 'fs'; +import path from 'path'; +import { getAppDataDir } from '../utils/appdata.js'; + +const defaultConfig = { + codexExe: "", + + // TODO: + // Save user-selected config options. Use these when starting Codex. + dataDir: "", + logsDir: "" + // storageQuota: 0, + // ports: { + // discPort: 8090, + // listenPort: 8070, + // apiPort: 8080 + // } +}; + +function getConfigFilename() { + return path.join(getAppDataDir(), "config.json"); +} + +export function saveConfig(config) { + const filePath = getConfigFilename(); + try { + fs.writeFileSync(filePath, JSON.stringify(config)); + } catch (error) { + console.error(`Failed to save config file to '${filePath}' error: '${error}'.`); + throw error; + } +} + +export function loadConfig() { + const filePath = getConfigFilename(); + try { + if (!fs.existsSync(filePath)) { + saveConfig(defaultConfig); + return defaultConfig; + } + return JSON.parse(fs.readFileSync(filePath)); + } catch (error) { + console.error(`Failed to load config file from '${filePath}' error: '${error}'.`); + throw error; + } +} diff --git a/src/services/nodeService.js b/src/services/nodeService.js index bba61ff..810760b 100644 --- a/src/services/nodeService.js +++ b/src/services/nodeService.js @@ -2,6 +2,7 @@ import axios from 'axios'; import { runCommand } from '../utils/command.js'; import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js'; import os from 'os'; +import { getCodexVersion } from '../handlers/installationHandlers.js'; const platform = os.platform(); @@ -29,10 +30,10 @@ export async function isNodeRunning() { } } -export async function isCodexInstalled() { +export async function isCodexInstalled(config) { try { - await runCommand('codex --version'); - return true; + const version = await getCodexVersion(config); + return version.length > 0; } catch (error) { return false; } diff --git a/src/utils/appdata.js b/src/utils/appdata.js new file mode 100644 index 0000000..1ff6059 --- /dev/null +++ b/src/utils/appdata.js @@ -0,0 +1,54 @@ +import path from 'path'; +import fs from 'fs'; + +export function getAppDataDir() { + return ensureExists(appData("codex-cli")); +} + +export function getCodexRootPath() { + return ensureExists(appData("codex")); +} + +export function getCodexBinPath() { + return ensureExists(path.join(appData("codex"), "bin")); +} + +export function getCodexDataDirDefaultPath() { + // This path does not exist on first startup. That's good: Codex will + // create it with the required access permissions. + return path.join(appData("codex"), "datadir"); +} + +export function getCodexLogsPath() { + return ensureExists(path.join(appData("codex"), "logs")); +} + +function ensureExists(dir) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + return dir; +} + +function appData(...app) { + let appData; + if (process.platform === 'win32') { + appData = path.join(process.env.APPDATA, ...app); + } else if (process.platform === 'darwin') { + appData = path.join(process.env.HOME, 'Library', 'Application Support', ...app); + } else { + appData = path.join(process.env.HOME, ...prependDot(...app)); + } + return appData; +} + +function prependDot(...app) { + return app.map((item, i) => { + if (i === 0) { + return `.${item}`; + } else { + return item; + } + }); +} + diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js new file mode 100644 index 0000000..172734f --- /dev/null +++ b/src/utils/pathSelector.js @@ -0,0 +1,169 @@ +import path from 'path'; +import inquirer from 'inquirer'; +import boxen from 'boxen'; +import chalk from 'chalk'; +import fs from 'fs'; + +function showMsg(msg) { + console.log(boxen(chalk.white(msg), { + padding: 1, + margin: 1, + borderStyle: 'round', + borderColor: 'white', + titleAlignment: 'center' + })); +} + +function splitPath(str) { + return str.replaceAll("\\", "/").split("/"); +} + +function showCurrent(currentPath) { + const len = currentPath.length; + showMsg(`Current path: [${len}]\n` + path.join(...currentPath)); +} + +async function showMain(currentPath) { + showCurrent(currentPath); + const { choice } = await inquirer.prompt([ + { + type: 'list', + name: 'choice', + message: 'Select an option:', + choices: [ + '1. Enter path', + '2. Go up one', + '3. Go down one', + '4. Check path exists', + '5. Create new folder here', + '6. Select this path', + '7. Cancel' + ], + pageSize: 6, + loop: true + } + ]).catch(() => { + handleExit(); + return { choice: '6' }; + }); + + return choice; +} + +export async function selectPath() { + var currentPath = splitPath(process.cwd()); + + while (true) { + const choice = await showMain(currentPath); + + switch (choice.split('.')[0]) { + case '1': + currentPath = await enterPath(); + break; + case '2': + currentPath = upOne(currentPath); + break; + case '3': + currentPath = await downOne(currentPath); + break; + case '4': + await checkExists(currentPath); + break; + case '5': + currentPath = await createSubDir(currentPath); + break; + case '6': + if (!isDir(currentPath)) { + console.log("Current path does not exist."); + } else { + return currentPath; + } + case '7': + return ""; + } + } +} + +async function enterPath() { + const response = await inquirer.prompt([ + { + type: 'input', + name: 'path', + message: 'Enter Path:' + }]); + + return splitPath(response.path); +} + +function upOne(currentPath) { + return currentPath.slice(0, currentPath.length - 1); +} + +function isDir(dir) { + return fs.lstatSync(dir).isDirectory(); +} + +function isSubDir(currentPath, entry) { + const newPath = path.join(...currentPath, entry); + return isDir(newPath); +} + +function getSubDirOptions(currentPath) { + const entries = fs.readdirSync(path.join(...currentPath)); + var result = []; + var counter = 1; + entries.forEach(function(entry) { + if (isSubDir(currentPath, entry)) { + result.push(counter + ". " + entry); + } + }); + return result; +} + +async function downOne(currentPath) { + const options = getSubDirOptions(currentPath); + if (options.length == 0) { + console.log("There are no subdirectories here."); + return currentPath; + } + + const { choice } = await inquirer.prompt([ + { + type: 'list', + name: 'choice', + message: 'Select an subdir:', + choices: options, + pageSize: options.length, + loop: true + } + ]).catch(() => { + return currentPath; + }); + + const subDir = choice.slice(3); + return [...currentPath, subDir]; +} + +async function checkExists(currentPath) { + if (!isDir(path.join(...currentPath))) { + console.log("Current path does not exist."); + } else{ + console.log("Current path exists."); + } +} + +async function createSubDir(currentPath) { + const response = await inquirer.prompt([ + { + type: 'input', + name: 'name', + message: 'Enter name:' + }]); + + const name = response.name; + if (name.length < 1) return; + + const fullDir = path.join(...currentPath, name); + fs.mkdirSync(fullDir); + return [...currentPath, name]; +}