Merge pull request #5 from codex-storage/feature/tool-config

Feature/tool config
This commit is contained in:
Guru 2025-02-25 03:34:34 +05:30 committed by GitHub
commit dfec8f7c08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 385 additions and 81 deletions

View File

@ -1,11 +1,15 @@
import { createSpinner } from 'nanospinner'; import path from 'path';
import { runCommand } from '../utils/command.js';
import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js';
import { checkDependencies, isCodexInstalled } from '../services/nodeService.js';
import inquirer from 'inquirer'; import inquirer from 'inquirer';
import boxen from 'boxen'; import boxen from 'boxen';
import chalk from 'chalk'; import chalk from 'chalk';
import os from 'os'; 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(); const platform = os.platform();
@ -52,19 +56,54 @@ These information will be used for calculating various metrics that can eventual
return agreement.toLowerCase() === 'y'; return agreement.toLowerCase() === 'y';
} }
export async function checkCodexInstallation(showNavigationMenu) { export async function getCodexVersion(config) {
if (config.codexExe.length < 1) return "";
try { try {
const version = await runCommand('codex --version'); const version = await runCommand(`"${config.codexExe}" --version`);
console.log(chalk.green('Codex is already installed. Version:')); if (version.length < 1) throw new Error("Version info not found.");
console.log(chalk.green(version)); return version;
await showNavigationMenu();
} catch (error) { } catch (error) {
console.log(chalk.cyanBright('Codex is not installed, proceeding with installation...')); return "";
await installCodex(showNavigationMenu);
} }
} }
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(); const agreed = await showPrivacyDisclaimer();
if (!agreed) { if (!agreed) {
console.log(showInfoMessage('You can find manual setup instructions at docs.codex.storage')); console.log(showInfoMessage('You can find manual setup instructions at docs.codex.storage'));
@ -72,40 +111,25 @@ export async function installCodex(showNavigationMenu) {
return; return;
} }
const installPath = getCodexBinPath();
console.log(showInfoMessage("Install location: " + installPath));
const spinner = createSpinner('Installing Codex...').start();
try { try {
const spinner = createSpinner('Downloading Codex binaries...').start();
if (platform === 'win32') { if (platform === 'win32') {
try { try {
try { try {
await runCommand('curl --version'); await runCommand('curl --version');
} catch (error) { } catch (error) {
spinner.error();
throw new Error('curl is not available. Please install curl or update your Windows version.'); 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('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 saveCodexExePathToConfig(config, path.join(installPath, "codex.exe"));
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'
));
}
try { try {
await runCommand('del /f install.cmd'); await runCommand('del /f install.cmd');
@ -113,23 +137,20 @@ export async function installCodex(showNavigationMenu) {
// Ignore cleanup errors // Ignore cleanup errors
} }
} catch (error) { } catch (error) {
spinner.error();
if (error.message.includes('Access is denied')) { if (error.message.includes('Access is denied')) {
throw new Error('Installation failed. Please run the command prompt as Administrator and try again.'); throw new Error('Installation failed. Please run the command prompt as Administrator and try again.');
} else if (error.message.includes('curl')) { } else if (error.message.includes('curl')) {
throw new Error(error.message); throw new Error(error.message);
} else { } else {
throw new Error('Installation failed. Please check your internet connection and try again.'); throw new Error(`Installation failed: "${error.message}"`);
} }
} }
} else { } else {
try { try {
const dependenciesInstalled = await checkDependencies(); const dependenciesInstalled = await checkDependencies();
if (!dependenciesInstalled) { if (!dependenciesInstalled) {
spinner.error();
console.log(showInfoMessage('Please install the required dependencies and try again.')); console.log(showInfoMessage('Please install the required dependencies and try again.'));
await showNavigationMenu(); throw new Error("Missing dependencies.");
return;
} }
const downloadCommand = 'curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh'; 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 { eval {
local $SIG{ALRM} = sub { die "timeout\\n" }; local $SIG{ALRM} = sub { die "timeout\\n" };
alarm(120); alarm(120);
system("bash install.sh"); system("INSTALL_DIR=\\"${installPath}\\" bash install.sh");
alarm(0); alarm(0);
}; };
die if $@; die if $@;
'`; '`;
await runCommand(timeoutCommand); await runCommand(timeoutCommand);
} else { } 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) { } catch (error) {
spinner.error();
if (error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')) { if (error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')) {
throw new Error('Installation failed. Please check your internet connection and try again.'); throw new Error('Installation failed. Please check your internet connection and try again.');
} else if (error.message.includes('Permission denied')) { } else if (error.message.includes('Permission denied')) {
@ -168,28 +189,38 @@ export async function installCodex(showNavigationMenu) {
} }
try { try {
const version = await runCommand('codex --version'); const version = await getCodexVersion(config);
console.log(showSuccessMessage( console.log(showSuccessMessage(
'Codex is successfully installed!\n\n' + 'Codex is successfully installed!\n' +
`Install path: "${config.codexExe}"\n\n` +
`Version: ${version}` `Version: ${version}`
)); ));
} catch (error) { } catch (error) {
throw new Error('Installation completed but Codex command is not available. Please restart your terminal and try again.'); throw new Error('Installation completed but Codex command is not available. Please restart your terminal and try again.');
} }
spinner.success();
await showNavigationMenu(); await showNavigationMenu();
} catch (error) { } catch (error) {
spinner.error();
console.log(showErrorMessage(`Failed to install Codex: ${error.message}`)); console.log(showErrorMessage(`Failed to install Codex: ${error.message}`));
await showNavigationMenu(); 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([ const { confirm } = await inquirer.prompt([
{ {
type: 'confirm', type: 'confirm',
name: '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 default: false
} }
]); ]);
@ -201,26 +232,9 @@ export async function uninstallCodex(showNavigationMenu) {
} }
try { try {
if (platform === 'win32') { removeDir(getCodexRootPath());
console.log(showInfoMessage('Removing Codex from Windows...')); clearCodexExePathFromConfig(config);
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}`);
}
console.log(showSuccessMessage('Codex has been successfully uninstalled.')); console.log(showSuccessMessage('Codex has been successfully uninstalled.'));
await showNavigationMenu(); await showNavigationMenu();
} catch (error) { } catch (error) {
@ -231,4 +245,4 @@ export async function uninstallCodex(showNavigationMenu) {
} }
await showNavigationMenu(); await showNavigationMenu();
} }
} }

View File

@ -1,7 +1,8 @@
import path from 'path';
import { createSpinner } from 'nanospinner'; import { createSpinner } from 'nanospinner';
import { runCommand } from '../utils/command.js'; import { runCommand } from '../utils/command.js';
import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.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 inquirer from 'inquirer';
import boxen from 'boxen'; import boxen from 'boxen';
import chalk from 'chalk'; import chalk from 'chalk';
@ -27,8 +28,15 @@ async function promptForWalletAddress() {
return wallet || null; return wallet || null;
} }
export async function runCodex(showNavigationMenu) { function getCurrentLogFile(config) {
const isInstalled = await isCodexInstalled(); 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) { if (!isInstalled) {
console.log(showErrorMessage('Codex is not installed. Please install Codex first using option 1 from the main menu.')); console.log(showErrorMessage('Codex is not installed. Please install Codex first using option 1 from the main menu.'));
await showNavigationMenu(); await showNavigationMenu();
@ -65,9 +73,19 @@ export async function runCodex(showNavigationMenu) {
nat = await runCommand('curl -s https://ip.codex.storage'); 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 = [ const args = [
`--data-dir=datadir`, `--data-dir="${config.dataDir}"`,
`--log-file="${logFilePath}"`,
`--disc-port=${discPort}`, `--disc-port=${discPort}`,
`--listen-addrs=/ip4/0.0.0.0/tcp/${listenPort}`, `--listen-addrs=/ip4/0.0.0.0/tcp/${listenPort}`,
`--nat=${nat}`, `--nat=${nat}`,
@ -76,10 +94,11 @@ export async function runCodex(showNavigationMenu) {
]; ];
const command = const command =
`${executable} ${args.join(" ")}` `"${executable}" ${args.join(" ")}`
console.log(showInfoMessage( console.log(showInfoMessage(
'🚀 Codex node is running...\n\n' + '🚀 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' + 'Please keep this terminal open. Start a new terminal to interact with the node.\n\n' +
'Press CTRL+C to stop the node' 'Press CTRL+C to stop the node'
)); ));

View File

@ -9,6 +9,7 @@ import { uploadFile, downloadFile, showLocalFiles } from './handlers/fileHandler
import { checkCodexInstallation, installCodex, uninstallCodex } from './handlers/installationHandlers.js'; import { checkCodexInstallation, installCodex, uninstallCodex } from './handlers/installationHandlers.js';
import { runCodex, checkNodeStatus } from './handlers/nodeHandlers.js'; import { runCodex, checkNodeStatus } from './handlers/nodeHandlers.js';
import { showInfoMessage } from './utils/messages.js'; import { showInfoMessage } from './utils/messages.js';
import { loadConfig } from './services/config.js';
async function showNavigationMenu() { async function showNavigationMenu() {
console.log('\n') console.log('\n')
@ -67,9 +68,9 @@ export async function main() {
process.on('SIGQUIT', handleExit); process.on('SIGQUIT', handleExit);
try { try {
const config = loadConfig();
while (true) { while (true) {
console.log('\n' + chalk.cyanBright(ASCII_ART)); console.log('\n' + chalk.cyanBright(ASCII_ART));
const { choice } = await inquirer.prompt([ const { choice } = await inquirer.prompt([
{ {
type: 'list', type: 'list',
@ -101,10 +102,10 @@ export async function main() {
switch (choice.split('.')[0]) { switch (choice.split('.')[0]) {
case '1': case '1':
await checkCodexInstallation(showNavigationMenu); await checkCodexInstallation(config, showNavigationMenu);
break; break;
case '2': case '2':
await runCodex(showNavigationMenu); await runCodex(config, showNavigationMenu);
return; return;
case '3': case '3':
await checkNodeStatus(showNavigationMenu); await checkNodeStatus(showNavigationMenu);
@ -119,7 +120,7 @@ export async function main() {
await showLocalFiles(showNavigationMenu); await showLocalFiles(showNavigationMenu);
break; break;
case '7': case '7':
await uninstallCodex(showNavigationMenu); await uninstallCodex(config, showNavigationMenu);
break; break;
case '8': case '8':
const { exec } = await import('child_process'); const { exec } = await import('child_process');

46
src/services/config.js Normal file
View File

@ -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;
}
}

View File

@ -2,6 +2,7 @@ import axios from 'axios';
import { runCommand } from '../utils/command.js'; import { runCommand } from '../utils/command.js';
import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js'; import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js';
import os from 'os'; import os from 'os';
import { getCodexVersion } from '../handlers/installationHandlers.js';
const platform = os.platform(); const platform = os.platform();
@ -29,10 +30,10 @@ export async function isNodeRunning() {
} }
} }
export async function isCodexInstalled() { export async function isCodexInstalled(config) {
try { try {
await runCommand('codex --version'); const version = await getCodexVersion(config);
return true; return version.length > 0;
} catch (error) { } catch (error) {
return false; return false;
} }

54
src/utils/appdata.js Normal file
View File

@ -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;
}
});
}

169
src/utils/pathSelector.js Normal file
View File

@ -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];
}