Merge pull request #2 from codex-storage/features/verification

file restructuring, fixing download issue, adding support for windows…
This commit is contained in:
Guru 2024-12-17 06:21:55 +05:30 committed by GitHub
commit 6ef43aa7b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1071 additions and 856 deletions

View File

@ -1,51 +1,77 @@
# Codex Storage CLI
![CodexCLI](/images/screenshot.png)
A command-line interface for interacting with Codex Storage.
This CLI is the easiest way to get started with [Codex](https://codex.storage). Get your node up and running in a matter of seconds and start interacting with the Codex Testnet by uploading and downloading files from the other peers in the network.
## Features
> ⚠️ Note : Codex is currently in testnet and there is no guarentee for files stored on the Codex test network. All the files uploaded to Codex are not encrypted by default and hence, the files are publicly accessible. By using this application, you agree to acknowledge the [disclaimer](https://github.com/codex-storage/codex-testnet-starter/blob/master/DISCLAIMER.md).
- Install and manage Codex node
- Run Codex node with custom configuration
- Check node status and connected peers
- Upload and download files
- View local data
- Cross-platform support (Windows, Linux, macOS)
## How it works?
Run the Codex Storage CLI in your terminal
## Installation
```bash
npm install -g codexstorage
```
Or run directly with npx:
```bash
npx codexstorage
```
#### Downloading and running the Codex node
## Usage
A CLI interface would appear with multiple options. Start by downloading the Codex binaries by using option 1.
### Interactive Mode
![Installing]()
Simply run:
Once you are done downloading the binaries, you can go ahead and try running the Codex node by choosing option 2. You will be asked to enter your listening (default : 8070) and discovery (default : 8090) ports during this step.
```bash
codexstorage
```
![Running]()
This will start the interactive CLI menu where you can:
1. Download and install Codex
2. Run Codex node
3. Check node status
4. Upload a file
5. Download a file
6. Show local data
7. Uninstall Codex node
#### Checking the node status
### Command Line Mode
Now that your node is running, keep the terminal window open and start another instance of the Codex CLI by using the first command. We will be using this terminal to interact with the Codex node that is active.
Upload a file:
```bash
codexstorage --upload <filename>
```
You can checkout the node status to ensure that the node is discoverable and connected to the other peers in the Codex test network.
Download a file:
```bash
codexstorage --download <cid>
```
![Status]()
## Requirements
If you face any issues (peer discovery, port forwarding etc.,), join our [discord server](https://discord.gg/codex-storage) and ask for help.
- Node.js 14 or higher
- For Linux users: libgomp1 library
- For Windows users: curl command available
#### Uploading and downloading files from the testnet
## Development
To upload a new file into the testnet, select option 4 and enter the file's relative path. A unique CID will be displayed once the file is uploaded.
1. Clone the repository
2. Install dependencies:
```bash
npm install
```
3. Run the CLI:
```bash
npm start
```
![Upload]()
## License
To download a file from the testnet, select option 5 and enter the file's CID.
![Download]()
If you wish to view all the files that are stored in your local node, choose option 6 to display all the available CIDs.
#### What's next?
More features will be added soon!
MIT

815
index.js
View File

@ -1,817 +1,6 @@
#!/usr/bin/env node
import inquirer from 'inquirer';
import { exec } from 'child_process';
import { promisify } from 'util';
import os from 'os';
import { createSpinner } from 'nanospinner';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import axios from 'axios';
import * as fs from 'fs/promises';
import boxen from 'boxen';
import { main } from './src/main.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const execAsync = promisify(exec);
const platform = os.platform();
const ASCII_ART = `
+--------------------------------------------------------------------+
| Docs : docs.codex.storage | Discord : discord.gg/codex-storage |
+--------------------------------------------------------------------+
`;
function showSuccessMessage(message) {
return boxen(chalk.green(message), {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'green',
title: '✅ SUCCESS',
titleAlignment: 'center'
});
}
function showErrorMessage(message) {
return boxen(chalk.red(message), {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'red',
title: '❌ ERROR',
titleAlignment: 'center'
});
}
function showInfoMessage(message) {
return boxen(chalk.cyan(message), {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'cyan',
title: ' INFO',
titleAlignment: 'center'
});
}
async function runCommand(command) {
try {
const { stdout, stderr } = await execAsync(command);
return stdout;
} catch (error) {
console.error('Error:', error.message);
throw error;
}
}
async function showNavigationMenu() {
console.log('\n')
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: 'What would you like to do?',
choices: [
'1. Back to main menu',
'2. Exit'
],
pageSize: 2,
loop: true
}
]);
switch (choice.split('.')[0]) {
case '1':
return main(); // Returns to main menu
case '2':
handleExit();
}
}
async function checkCodexInstallation() {
try {
const version = await runCommand('codex --version');
console.log(chalk.green('Codex is already installed. Version:'));
console.log(chalk.green(version));
await showNavigationMenu();
} catch (error) {
console.log(chalk.cyanBright('Codex is not installed, proceeding with installation...'));
await installCodex();
}
}
async function showPrivacyDisclaimer() {
const disclaimer = boxen(`
${chalk.yellow.bold('Privacy Disclaimer')}
Codex is currently in testnet and to make your testnet experience better, we are currently tracking some of your node and network information such as:
${chalk.cyan('- Node ID')}
${chalk.cyan('- Peer ID')}
${chalk.cyan('- Public IP address')}
${chalk.cyan('- Codex node version')}
${chalk.cyan('- Number of connected peers')}
${chalk.cyan('- Discovery and listening ports')}
These information will be used for calculating various metrics that can eventually make the Codex experience better. Please agree to the following disclaimer to continue using the Codex Storage CLI or alternatively, use the manual setup instructions at docs.codex.storage.
`, {
padding: 1,
margin: 1,
borderStyle: 'double',
borderColor: 'yellow',
title: '📋 IMPORTANT',
titleAlignment: 'center'
});
console.log(disclaimer);
const { agreement } = await inquirer.prompt([
{
type: 'input',
name: 'agreement',
message: 'Do you agree to the privacy disclaimer? (y/n):',
validate: (input) => {
const lowercased = input.toLowerCase();
if (lowercased === 'y' || lowercased === 'n') {
return true;
}
return 'Please enter either y or n';
}
}
]);
return agreement.toLowerCase() === 'y';
}
async function checkDependencies() {
if (platform === 'linux') {
try {
await runCommand('ldconfig -p | grep libgomp');
return true;
} catch (error) {
console.log(showErrorMessage('Required dependency libgomp1 is not installed.'));
console.log(showInfoMessage(
'For Debian-based Linux systems, please install it manually using:\n\n' +
chalk.white('sudo apt update && sudo apt install libgomp1')
));
return false;
}
}
return true;
}
async function installCodex() {
if (platform === 'win32') {
console.log(showInfoMessage('Coming soon for Windows!'));
return;
}
// Show privacy disclaimer first
const agreed = await showPrivacyDisclaimer();
if (!agreed) {
console.log(showInfoMessage('You can find manual setup instructions at docs.codex.storage'));
handleExit();
return;
}
try {
// Check dependencies only for Linux
const dependenciesInstalled = await checkDependencies();
if (!dependenciesInstalled) {
console.log(showInfoMessage('Please install the required dependencies and try again.'));
await showNavigationMenu();
return;
}
const spinner = createSpinner('Downloading Codex binaries...').start();
const downloadCommand = 'curl -# -L https://get.codex.storage/install.sh | bash';
await runCommand(downloadCommand);
spinner.success();
const version = await runCommand('\ncodex --version');
console.log(showSuccessMessage(
'Codex is successfully installed!\n\n' +
`Version: ${version}`
));
await showNavigationMenu();
} catch (error) {
console.log(showErrorMessage(`Failed to install Codex: ${error.message}`));
await showNavigationMenu();
}
}
async function isNodeRunning() {
try {
const response = await axios.get('http://localhost:8080/api/codex/v1/debug/info');
return response.status === 200;
} catch (error) {
return false;
}
}
async function isCodexInstalled() {
try {
await runCommand('codex --version');
return true;
} catch (error) {
return false;
}
}
async function runCodex() {
// First check if Codex is installed
const isInstalled = await isCodexInstalled();
if (!isInstalled) {
console.log(showErrorMessage('Codex is not installed. Please install Codex first using option 1 from the main menu.'));
await showNavigationMenu();
return;
}
const nodeAlreadyRunning = await isNodeRunning();
if (nodeAlreadyRunning) {
console.log(showInfoMessage('A Codex node is already running.'));
await showNavigationMenu();
} else {
const { discPort, listenPort } = await inquirer.prompt([
{
type: 'number',
name: 'discPort',
message: 'Enter the discovery port (default is 8090):',
default: 8090
},
{
type: 'number',
name: 'listenPort',
message: 'Enter the listening port (default is 8070):',
default: 8070
}
]);
try {
const command = `codex \
--data-dir=datadir \
--disc-port=${discPort} \
--listen-addrs=/ip4/0.0.0.0/tcp/${listenPort} \
--nat=\`curl -s https://ip.codex.storage\` \
--api-cors-origin="*" \
--bootstrap-node=spr:CiUIAhIhAiJvIcA_ZwPZ9ugVKDbmqwhJZaig5zKyLiuaicRcCGqLEgIDARo8CicAJQgCEiECIm8hwD9nA9n26BUoNuarCEllqKDnMrIuK5qJxFwIaosQ3d6esAYaCwoJBJ_f8zKRAnU6KkYwRAIgM0MvWNJL296kJ9gWvfatfmVvT-A7O2s8Mxp8l9c8EW0CIC-h-H-jBVSgFjg3Eny2u33qF7BDnWFzo7fGfZ7_qc9P`;
console.log(showInfoMessage(
'🚀 Codex node is running...\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'
));
// Start the node
const nodeProcess = exec(command);
// Wait for node to start and get initial data
await new Promise(resolve => setTimeout(resolve, 5000));
// Log node data to Supabase silently
try {
const response = await axios.get('http://localhost:8080/api/codex/v1/debug/info');
if (response.status === 200) {
await logToSupabase(response.data);
// Show privacy notice after successful logging
console.log(boxen(
chalk.cyan('We are logging some of your node\'s public data for improving the Codex experience'),
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'cyan',
title: '🔒 Privacy Notice',
titleAlignment: 'center',
dimBorder: true
}
));
}
} catch (error) {
// Silently handle any logging errors
}
// Wait for node process to exit
await new Promise((resolve, reject) => {
nodeProcess.on('exit', (code) => {
if (code === 0) resolve();
else reject(new Error(`Node exited with code ${code}`));
});
});
} catch (error) {
console.log(showErrorMessage(`Failed to run Codex: ${error.message}`));
}
await showNavigationMenu();
}
}
async function logToSupabase(nodeData) {
try {
// Ensure peerCount is at least 0 (not undefined or null)
const peerCount = nodeData.table.nodes ? nodeData.table.nodes.length : "0";
const payload = {
nodeId: nodeData.table.localNode.nodeId,
peerId: nodeData.table.localNode.peerId,
publicIp: nodeData.announceAddresses[0].split('/')[2],
version: nodeData.codex.version,
peerCount: peerCount == 0 ? "0" : peerCount,
port: nodeData.announceAddresses[0].split('/')[4],
listeningAddress: nodeData.table.localNode.address
};
const response = await axios.post('https://vfcnsjxahocmzefhckfz.supabase.co/functions/v1/codexnodes', payload, {
headers: {
'Content-Type': 'application/json'
}
});
if (response.status === 200) {
return true;
} else {
console.error('Unexpected response:', response.status, response.data);
return false;
}
} catch (error) {
console.error('Failed to log to Supabase:', error.message);
if (error.response) {
console.error('Error response:', {
status: error.response.status,
data: error.response.data
});
}
return false;
}
}
async function showNodeDetails(data) {
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: 'Select information to view:',
choices: [
'1. View Connected Peers',
'2. View Node Information',
'3. Back to Main Menu',
'4. Exit'
],
pageSize: 4,
loop: true
}
]);
switch (choice.split('.')[0].trim()) {
case '1':
const peerCount = data.table.nodes.length;
if (peerCount > 0) {
console.log(showInfoMessage('Connected Peers'));
data.table.nodes.forEach((node, index) => {
console.log(boxen(
`Peer ${index + 1}:\n` +
`${chalk.cyan('Peer ID:')} ${node.peerId}\n` +
`${chalk.cyan('Address:')} ${node.address}\n` +
`${chalk.cyan('Status:')} ${node.seen ? chalk.green('Active') : chalk.gray('Inactive')}`,
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'blue'
}
));
});
} else {
console.log(showInfoMessage('No connected peers found.'));
}
return showNodeDetails(data);
case '2':
console.log(boxen(
`${chalk.cyan('Version:')} ${data.codex.version}\n` +
`${chalk.cyan('Revision:')} ${data.codex.revision}\n\n` +
`${chalk.cyan('Node ID:')} ${data.table.localNode.nodeId}\n` +
`${chalk.cyan('Peer ID:')} ${data.table.localNode.peerId}\n` +
`${chalk.cyan('Listening Address:')} ${data.table.localNode.address}\n\n` +
`${chalk.cyan('Public IP:')} ${data.announceAddresses[0].split('/')[2]}\n` +
`${chalk.cyan('Port:')} ${data.announceAddresses[0].split('/')[4]}\n` +
`${chalk.cyan('Connected Peers:')} ${data.table.nodes.length}`,
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'yellow',
title: '📊 Node Information',
titleAlignment: 'center'
}
));
return showNodeDetails(data);
case '3':
return main();
case '4':
handleExit();
break;
}
}
async function checkNodeStatus() {
if (platform === 'win32') {
console.log(showInfoMessage('Coming soon for Windows!'));
return;
}
try {
const nodeRunning = await isNodeRunning();
if (nodeRunning) {
const spinner = createSpinner('Checking node status...').start();
const response = await runCommand('curl http://localhost:8080/api/codex/v1/debug/info -w \'\\n\'');
spinner.success();
// Parse the JSON response
const data = JSON.parse(response);
// Determine if node is online and discoverable
const peerCount = data.table.nodes.length;
const isOnline = peerCount > 2;
// Show status banner
console.log(boxen(
isOnline
? chalk.green('Node is ONLINE & DISCOVERABLE')
: chalk.yellow('Node is ONLINE but has few peers'),
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: isOnline ? 'green' : 'yellow',
title: '🔌 Node Status',
titleAlignment: 'center'
}
));
// Show interactive menu for details
await showNodeDetails(data);
} else {
console.log(showErrorMessage('Codex node is not running. Try again after starting the node'));
await showNavigationMenu();
}
} catch (error) {
console.log(showErrorMessage(`Failed to check node status: ${error.message}`));
await showNavigationMenu();
}
}
// Define this function once, near the top of the file
function handleCommandLineOperation() {
return process.argv.length > 2;
}
async function uploadFile(filePath = null) {
if (platform === 'win32') {
console.log(showInfoMessage('Coming soon for Windows!'));
return;
}
const nodeRunning = await isNodeRunning();
if (!nodeRunning) {
console.log(showErrorMessage('Codex node is not running. Try again after starting the node'));
return handleCommandLineOperation() ? process.exit(1) : showNavigationMenu();
}
console.log(boxen(
chalk.yellow('⚠️ Codex does not encrypt files. Anything uploaded will be available publicly on testnet.\nThe testnet does not provide any guarantees - please do not use in production.'),
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'yellow',
title: '⚠️ Warning',
titleAlignment: 'center'
}
));
let fileToUpload = filePath;
if (!fileToUpload) {
const { inputPath } = await inquirer.prompt([
{
type: 'input',
name: 'inputPath',
message: 'Enter the file path to upload:',
validate: input => input.length > 0
}
]);
fileToUpload = inputPath;
}
try {
// Check if file exists
await fs.access(fileToUpload);
const spinner = createSpinner('Uploading file').start();
try {
const result = await runCommand(`curl -X POST http://localhost:8080/api/codex/v1/data -H 'Content-Type: application/octet-stream' -w '\\n' -T ${fileToUpload}`);
spinner.success();
console.log(showSuccessMessage('Successfully uploaded!\n\nCID: ' + result.trim()));
} catch (error) {
spinner.error();
throw new Error(`Failed to upload: ${error.message}`);
}
} catch (error) {
console.log(showErrorMessage(error.code === 'ENOENT'
? `File not found: ${fileToUpload}`
: `Error uploading file: ${error.message}`));
}
return handleCommandLineOperation() ? process.exit(0) : showNavigationMenu();
}
function parseCommandLineArgs() {
const args = process.argv.slice(2);
if (args.length === 0) return null;
switch (args[0]) {
case '--upload':
if (args.length !== 2) {
console.log(showErrorMessage('Usage: npx codexstorage --upload <filename>'));
process.exit(1);
}
return { command: 'upload', value: args[1] };
case '--download':
if (args.length !== 2) {
console.log(showErrorMessage('Usage: npx codexstorage --download <cid>'));
process.exit(1);
}
return { command: 'download', value: args[1] };
default:
console.log(showErrorMessage(
'Invalid command. Available commands:\n\n' +
'npx codexstorage\n' +
'npx codexstorage --upload <filename>\n' +
'npx codexstorage --download <cid>'
));
process.exit(1);
}
}
async function downloadFile(cid = null) {
if (platform === 'win32') {
console.log(showInfoMessage('Coming soon for Windows!'));
return;
}
const nodeRunning = await isNodeRunning();
if (!nodeRunning) {
console.log(showErrorMessage('Codex node is not running. Try again after starting the node'));
return handleCommandLineOperation() ? process.exit(1) : showNavigationMenu();
}
let cidToDownload = cid;
if (!cidToDownload) {
const { inputCid } = await inquirer.prompt([
{
type: 'input',
name: 'inputCid',
message: 'Enter the CID:',
validate: input => input.length > 0
}
]);
cidToDownload = inputCid;
}
try {
const spinner = createSpinner('Downloading file').start();
try {
await runCommand(`curl "http://localhost:8080/api/codex/v1/data/${cidToDownload}/network/stream" -o "${cidToDownload}.png"`);
spinner.success();
console.log(showSuccessMessage(`Successfully downloaded!\n\nFile saved as ${cidToDownload}.png`));
} catch (error) {
spinner.error();
throw new Error(`Failed to download: ${error.message}`);
}
} catch (error) {
console.log(showErrorMessage(`Error downloading file: ${error.message}`));
}
return handleCommandLineOperation() ? process.exit(0) : showNavigationMenu();
}
async function showLocalFiles() {
if (platform === 'win32') {
console.log(showInfoMessage('Coming soon for Windows!'));
return;
}
const nodeRunning = await isNodeRunning();
if (!nodeRunning) {
console.log(showErrorMessage('Codex node is not running. Try again after starting the node'));
await showNavigationMenu();
return;
}
try {
const spinner = createSpinner('Fetching local files...').start();
const filesResponse = await runCommand('curl http://localhost:8080/api/codex/v1/data -w \'\\n\'');
spinner.success();
// Parse the JSON response
const filesData = JSON.parse(filesResponse);
if (filesData.content && filesData.content.length > 0) {
console.log(showInfoMessage(`Found ${filesData.content.length} local file(s)`));
// Iterate through each file and display information
filesData.content.forEach((file, index) => {
const { cid, manifest } = file;
const { rootHash, originalBytes, blockSize, protected: isProtected, filename, mimetype, uploadedAt } = manifest;
// Convert the UNIX timestamp to a readable format
const uploadedDate = new Date(uploadedAt * 1000).toLocaleString();
const fileSize = (originalBytes / 1024).toFixed(2); // Convert to KB
console.log(boxen(
`${chalk.cyan('File')} ${index + 1} of ${filesData.content.length}\n\n` +
`${chalk.cyan('Filename:')} ${filename}\n` +
`${chalk.cyan('CID:')} ${cid}\n` +
`${chalk.cyan('Size:')} ${fileSize} KB\n` +
`${chalk.cyan('MIME Type:')} ${mimetype}\n` +
`${chalk.cyan('Uploaded:')} ${uploadedDate}\n` +
`${chalk.cyan('Protected:')} ${isProtected ? chalk.green('Yes') : chalk.red('No')}`,
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'blue',
title: `📁 File Details`,
titleAlignment: 'center'
}
));
});
} else {
console.log(showInfoMessage('No local files found.'));
}
await showNavigationMenu();
} catch (error) {
console.log(showErrorMessage(`Failed to show local files: ${error.message}`));
await showNavigationMenu();
}
}
async function uninstallCodex() {
const binaryPath = '/usr/local/bin/codex';
// Ask for confirmation
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.'),
default: false
}
]);
if (!confirm) {
console.log(showInfoMessage('Uninstall cancelled.'));
await showNavigationMenu();
return;
}
try {
console.log(showInfoMessage(`Attempting to remove Codex binary from ${binaryPath}...`));
await runCommand(`sudo rm ${binaryPath}`);
console.log(showSuccessMessage('Codex has been successfully uninstalled.'));
await showNavigationMenu();
} catch (error) {
if (error.code === 'ENOENT') {
console.log(showInfoMessage('Codex binary not found, nothing to uninstall.'));
} else {
console.log(showErrorMessage('Failed to uninstall Codex. Please make sure Codex is installed before trying to remove it.'));
}
await showNavigationMenu();
}
}
// Add this function for cleanup and goodbye
function handleExit() {
console.log(boxen(
chalk.cyanBright('👋 Thank you for using Codex Storage CLI! Goodbye!'),
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'cyan',
title: '👋 GOODBYE',
titleAlignment: 'center'
}
));
process.exit(0);
}
// Add signal handlers at the start of main
async function main() {
// Handle command line arguments
const commandArgs = parseCommandLineArgs();
if (commandArgs) {
switch (commandArgs.command) {
case 'upload':
await uploadFile(commandArgs.value);
return;
case 'download':
await downloadFile(commandArgs.value);
return;
}
}
// Handle exit signals
process.on('SIGINT', handleExit);
process.on('SIGTERM', handleExit);
process.on('SIGQUIT', handleExit);
try {
while (true) {
console.log('\n' + chalk.cyanBright(ASCII_ART));
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: 'Select an option:',
choices: [
'1. Download and install Codex',
'2. Run Codex node',
'3. Check node status',
'4. Upload a file',
'5. Download a file',
'6. Show local data',
'7. Uninstall Codex node',
'8. Exit'
],
pageSize: 8,
loop: true
}
]).catch(() => {
handleExit();
return { choice: '8' };
});
if (choice.startsWith('8')) {
handleExit();
break;
}
switch (choice.split('.')[0]) {
case '1':
await checkCodexInstallation();
break;
case '2':
await runCodex();
return;
case '3':
await checkNodeStatus();
break;
case '4':
await uploadFile();
break;
case '5':
await downloadFile();
break;
case '6':
await showLocalFiles();
break;
case '7':
await uninstallCodex();
break;
}
console.log('\n'); // Add some spacing between operations
}
} catch (error) {
if (error.message.includes('ExitPromptError')) {
handleExit();
} else {
console.error(chalk.red('An error occurred:', error.message));
handleExit();
}
}
}
// Run the CLI
// Start the CLI application
main().catch(console.error);

34
package-lock.json generated
View File

@ -1,24 +1,39 @@
{
"name": "codexstorage",
"version": "1.0.4",
"version": "1.0.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codexstorage",
"version": "1.0.4",
"version": "1.0.5",
"license": "ISC",
"dependencies": {
"@codex-storage/sdk-js": "^0.0.16",
"axios": "^1.7.7",
"boxen": "^8.0.1",
"chalk": "^5.3.0",
"form-data": "^4.0.1",
"inquirer": "^12.0.1",
"nanospinner": "^1.1.0"
"mime-types": "^2.1.35",
"nanospinner": "^1.1.0",
"xhr2": "^0.2.1"
},
"bin": {
"codexstorage": "index.js"
}
},
"node_modules/@codex-storage/sdk-js": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@codex-storage/sdk-js/-/sdk-js-0.0.16.tgz",
"integrity": "sha512-aeb2likTXXL8oE1P5c2c54NluJ5NcGqhfihE3/3AXHo1U+Y3ZXDRuKpPpNesGjGqs6EDuLAm8b+g0FJjJBhnbQ==",
"dependencies": {
"valibot": "^0.32.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@inquirer/checkbox": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.1.tgz",
@ -738,6 +753,11 @@
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"peer": true
},
"node_modules/valibot": {
"version": "0.32.0",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-0.32.0.tgz",
"integrity": "sha512-FXBnJl4bNOmeg7lQv+jfvo/wADsRBN8e9C3r+O77Re3dEnDma8opp7p4hcIbF7XJJ30h/5SVohdjer17/sHOsQ=="
},
"node_modules/widest-line": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz",
@ -811,6 +831,14 @@
"node": ">=8"
}
},
"node_modules/xhr2": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz",
"integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==",
"engines": {
"node": ">= 6"
}
},
"node_modules/yoctocolors-cjs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz",

View File

@ -1,23 +1,28 @@
{
"name": "codexstorage",
"version": "1.0.4",
"description": "",
"type": "module",
"version": "1.0.0",
"description": "CLI tool for Codex Storage",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"type": "module",
"bin": {
"codexstorage": "./index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"scripts": {
"start": "node index.js"
},
"keywords": [
"codex",
"storage",
"cli"
],
"author": "Codex Storage",
"license": "MIT",
"dependencies": {
"axios": "^1.7.7",
"boxen": "^8.0.1",
"axios": "^1.6.2",
"boxen": "^7.1.1",
"chalk": "^5.3.0",
"inquirer": "^12.0.1",
"inquirer": "^9.2.12",
"mime-types": "^2.1.35",
"nanospinner": "^1.1.0"
}
}

35
src/cli/commandParser.js Normal file
View File

@ -0,0 +1,35 @@
import { showErrorMessage } from '../utils/messages.js';
export function handleCommandLineOperation() {
return process.argv.length > 2;
}
export function parseCommandLineArgs() {
const args = process.argv.slice(2);
if (args.length === 0) return null;
switch (args[0]) {
case '--upload':
if (args.length !== 2) {
console.log(showErrorMessage('Usage: npx codexstorage --upload <filename>'));
process.exit(1);
}
return { command: 'upload', value: args[1] };
case '--download':
if (args.length !== 2) {
console.log(showErrorMessage('Usage: npx codexstorage --download <cid>'));
process.exit(1);
}
return { command: 'download', value: args[1] };
default:
console.log(showErrorMessage(
'Invalid command. Available commands:\n\n' +
'npx codexstorage\n' +
'npx codexstorage --upload <filename>\n' +
'npx codexstorage --download <cid>'
));
process.exit(1);
}
}

19
src/constants/ascii.js Normal file
View File

@ -0,0 +1,19 @@
export const ASCII_ART = `
<EFBFBD><EFBFBD>
+--------------------------------------------------------------------+
| Docs : docs.codex.storage | Discord : discord.gg/codex-storage |
+--------------------------------------------------------------------+
`;

View File

@ -0,0 +1,196 @@
import { createSpinner } from 'nanospinner';
import { runCommand } from '../utils/command.js';
import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js';
import { isNodeRunning } from '../services/nodeService.js';
import fs from 'fs/promises';
import boxen from 'boxen';
import chalk from 'chalk';
import inquirer from 'inquirer';
import path from 'path';
import mime from 'mime-types';
import axios from 'axios';
export async function uploadFile(filePath = null, handleCommandLineOperation, showNavigationMenu) {
const nodeRunning = await isNodeRunning();
if (!nodeRunning) {
console.log(showErrorMessage('Codex node is not running. Try again after starting the node'));
return handleCommandLineOperation() ? process.exit(1) : showNavigationMenu();
}
console.log(boxen(
chalk.yellow('⚠️ Codex does not encrypt files. Anything uploaded will be available publicly on testnet.\nThe testnet does not provide any guarantees - please do not use in production.'),
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'yellow',
title: '⚠️ Warning',
titleAlignment: 'center'
}
));
let fileToUpload = filePath;
if (!fileToUpload) {
const { inputPath } = await inquirer.prompt([
{
type: 'input',
name: 'inputPath',
message: 'Enter the file path to upload:',
validate: input => input.length > 0
}
]);
fileToUpload = inputPath;
}
try {
await fs.access(fileToUpload);
const filename = path.basename(fileToUpload);
const contentType = mime.lookup(fileToUpload) || 'application/octet-stream';
const spinner = createSpinner('Uploading file').start();
try {
const result = await runCommand(
`curl -X POST http://localhost:8080/api/codex/v1/data ` +
`-H 'Content-Type: ${contentType}' ` +
`-H 'Content-Disposition: attachment; filename="${filename}"' ` +
`-w '\\n' -T "${fileToUpload}"`
);
spinner.success();
console.log(showSuccessMessage('Successfully uploaded!\n\nCID: ' + result.trim()));
} catch (error) {
spinner.error();
throw new Error(`Failed to upload: ${error.message}`);
}
} catch (error) {
console.log(showErrorMessage(error.code === 'ENOENT'
? `File not found: ${fileToUpload}`
: `Error uploading file: ${error.message}`));
}
return handleCommandLineOperation() ? process.exit(0) : showNavigationMenu();
}
export async function downloadFile(cid = null, handleCommandLineOperation, showNavigationMenu) {
const nodeRunning = await isNodeRunning();
if (!nodeRunning) {
console.log(showErrorMessage('Codex node is not running. Try again after starting the node'));
return handleCommandLineOperation() ? process.exit(1) : showNavigationMenu();
}
let cidToDownload = cid;
if (!cidToDownload) {
const { inputCid } = await inquirer.prompt([
{
type: 'input',
name: 'inputCid',
message: 'Enter the CID:',
validate: input => input.length > 0
}
]);
cidToDownload = inputCid;
}
try {
const spinner = createSpinner('Fetching file metadata...').start();
try {
// First, get the file metadata
const metadataResponse = await axios.post(`http://localhost:8080/api/codex/v1/data/${cidToDownload}/network`);
const { manifest } = metadataResponse.data;
const { filename, mimetype } = manifest;
spinner.success();
spinner.start('Downloading file...');
// Then download the file with the correct filename
await runCommand(`curl "http://localhost:8080/api/codex/v1/data/${cidToDownload}/network/stream" -o "${filename}"`);
spinner.success();
console.log(showSuccessMessage(
'Successfully downloaded!\n\n' +
`Filename: ${filename}\n` +
`Type: ${mimetype}`
));
// Show file details
console.log(boxen(
`${chalk.cyan('File Details')}\n\n` +
`${chalk.cyan('Filename:')} ${filename}\n` +
`${chalk.cyan('MIME Type:')} ${mimetype}\n` +
`${chalk.cyan('CID:')} ${cidToDownload}\n` +
`${chalk.cyan('Protected:')} ${manifest.protected ? chalk.green('Yes') : chalk.red('No')}\n` +
`${chalk.cyan('Uploaded:')} ${new Date(manifest.uploadedAt * 1000).toLocaleString()}`,
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'blue',
title: '📁 Download Complete',
titleAlignment: 'center'
}
));
} catch (error) {
spinner.error();
if (error.response) {
throw new Error(`Failed to download: ${error.response.data.message || 'File not found'}`);
} else {
throw new Error(`Failed to download: ${error.message}`);
}
}
} catch (error) {
console.log(showErrorMessage(`Error downloading file: ${error.message}`));
}
return handleCommandLineOperation() ? process.exit(0) : showNavigationMenu();
}
export async function showLocalFiles(showNavigationMenu) {
const nodeRunning = await isNodeRunning();
if (!nodeRunning) {
console.log(showErrorMessage('Codex node is not running. Try again after starting the node'));
await showNavigationMenu();
return;
}
try {
const spinner = createSpinner('Fetching local files...').start();
const filesResponse = await runCommand('curl http://localhost:8080/api/codex/v1/data -w \'\\n\'');
spinner.success();
const filesData = JSON.parse(filesResponse);
if (filesData.content && filesData.content.length > 0) {
console.log(showInfoMessage(`Found ${filesData.content.length} local file(s)`));
filesData.content.forEach((file, index) => {
const { cid, manifest } = file;
const { rootHash, originalBytes, blockSize, protected: isProtected, filename, mimetype, uploadedAt } = manifest;
const uploadedDate = new Date(uploadedAt * 1000).toLocaleString();
const fileSize = (originalBytes / 1024).toFixed(2);
console.log(boxen(
`${chalk.cyan('File')} ${index + 1} of ${filesData.content.length}\n\n` +
`${chalk.cyan('Filename:')} ${filename}\n` +
`${chalk.cyan('CID:')} ${cid}\n` +
`${chalk.cyan('Size:')} ${fileSize} KB\n` +
`${chalk.cyan('MIME Type:')} ${mimetype}\n` +
`${chalk.cyan('Uploaded:')} ${uploadedDate}\n` +
`${chalk.cyan('Protected:')} ${isProtected ? chalk.green('Yes') : chalk.red('No')}`,
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'blue',
title: `📁 File Details`,
titleAlignment: 'center'
}
));
});
}
} catch (error) {
console.log(showErrorMessage(`Failed to fetch local files: ${error.message}`));
}
await showNavigationMenu();
}

View File

@ -0,0 +1,232 @@
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 inquirer from 'inquirer';
import boxen from 'boxen';
import chalk from 'chalk';
import os from 'os';
const platform = os.platform();
async function showPrivacyDisclaimer() {
const disclaimer = boxen(`
${chalk.yellow.bold('Privacy Disclaimer')}
Codex is currently in testnet and to make your testnet experience better, we are currently tracking some of your node and network information such as:
${chalk.cyan('- Node ID')}
${chalk.cyan('- Peer ID')}
${chalk.cyan('- Public IP address')}
${chalk.cyan('- Codex node version')}
${chalk.cyan('- Number of connected peers')}
${chalk.cyan('- Discovery and listening ports')}
These information will be used for calculating various metrics that can eventually make the Codex experience better. Please agree to the following disclaimer to continue using the Codex Storage CLI or alternatively, use the manual setup instructions at docs.codex.storage.
`, {
padding: 1,
margin: 1,
borderStyle: 'double',
borderColor: 'yellow',
title: '📋 IMPORTANT',
titleAlignment: 'center'
});
console.log(disclaimer);
const { agreement } = await inquirer.prompt([
{
type: 'input',
name: 'agreement',
message: 'Do you agree to the privacy disclaimer? (y/n):',
validate: (input) => {
const lowercased = input.toLowerCase();
if (lowercased === 'y' || lowercased === 'n') {
return true;
}
return 'Please enter either y or n';
}
}
]);
return agreement.toLowerCase() === 'y';
}
export async function checkCodexInstallation(showNavigationMenu) {
try {
const version = await runCommand('codex --version');
console.log(chalk.green('Codex is already installed. Version:'));
console.log(chalk.green(version));
await showNavigationMenu();
} catch (error) {
console.log(chalk.cyanBright('Codex is not installed, proceeding with installation...'));
await installCodex(showNavigationMenu);
}
}
export async function installCodex(showNavigationMenu) {
const agreed = await showPrivacyDisclaimer();
if (!agreed) {
console.log(showInfoMessage('You can find manual setup instructions at docs.codex.storage'));
process.exit(0);
return;
}
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');
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!'));
} 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 {
await runCommand('del /f install.cmd');
} catch (error) {
// 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.');
}
}
} else {
try {
const dependenciesInstalled = await checkDependencies();
if (!dependenciesInstalled) {
spinner.error();
console.log(showInfoMessage('Please install the required dependencies and try again.'));
await showNavigationMenu();
return;
}
const downloadCommand = 'curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh';
await runCommand(downloadCommand);
if (platform === 'darwin') {
const timeoutCommand = `perl -e '
eval {
local $SIG{ALRM} = sub { die "timeout\\n" };
alarm(120);
system("bash install.sh");
alarm(0);
};
die if $@;
'`;
await runCommand(timeoutCommand);
} else {
await runCommand('timeout 120 bash install.sh');
}
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')) {
throw new Error('Permission denied. Please try running the command with sudo.');
} else if (error.message.includes('timeout')) {
throw new Error('Installation is taking longer than expected. Please try running with sudo.');
} else {
throw new Error('Installation failed. Please try running with sudo if you haven\'t already.');
}
} finally {
await runCommand('rm -f install.sh').catch(() => {});
}
}
try {
const version = await runCommand('codex --version');
console.log(showSuccessMessage(
'Codex is successfully installed!\n\n' +
`Version: ${version}`
));
} catch (error) {
throw new Error('Installation completed but Codex command is not available. Please restart your terminal and try again.');
}
await showNavigationMenu();
} catch (error) {
console.log(showErrorMessage(`Failed to install Codex: ${error.message}`));
await showNavigationMenu();
}
}
export async function uninstallCodex(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.'),
default: false
}
]);
if (!confirm) {
console.log(showInfoMessage('Uninstall cancelled.'));
await showNavigationMenu();
return;
}
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}`);
}
console.log(showSuccessMessage('Codex has been successfully uninstalled.'));
await showNavigationMenu();
} catch (error) {
if (error.code === 'ENOENT') {
console.log(showInfoMessage('Codex binary not found, nothing to uninstall.'));
} else {
console.log(showErrorMessage('Failed to uninstall Codex. Please make sure Codex is installed before trying to remove it.'));
}
await showNavigationMenu();
}
}

View File

@ -0,0 +1,229 @@
import { createSpinner } from 'nanospinner';
import { runCommand } from '../utils/command.js';
import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js';
import { isNodeRunning, isCodexInstalled, logToSupabase } from '../services/nodeService.js';
import inquirer from 'inquirer';
import boxen from 'boxen';
import chalk from 'chalk';
import os from 'os';
import { exec } from 'child_process';
import axios from 'axios';
const platform = os.platform();
export async function runCodex(showNavigationMenu) {
const isInstalled = await isCodexInstalled();
if (!isInstalled) {
console.log(showErrorMessage('Codex is not installed. Please install Codex first using option 1 from the main menu.'));
await showNavigationMenu();
return;
}
const nodeAlreadyRunning = await isNodeRunning();
if (nodeAlreadyRunning) {
console.log(showInfoMessage('A Codex node is already running.'));
await showNavigationMenu();
} else {
const { discPort, listenPort } = await inquirer.prompt([
{
type: 'number',
name: 'discPort',
message: 'Enter the discovery port (default is 8090):',
default: 8090
},
{
type: 'number',
name: 'listenPort',
message: 'Enter the listening port (default is 8070):',
default: 8070
}
]);
try {
if (platform === 'win32') {
console.log(showInfoMessage('Setting up firewall rules...'));
await runCommand(`netsh advfirewall firewall add rule name="Allow Codex (TCP-In)" protocol=TCP dir=in localport=${listenPort} action=allow`);
await runCommand(`netsh advfirewall firewall add rule name="Allow Codex (UDP-In)" protocol=UDP dir=in localport=${discPort} action=allow`);
}
let nat;
if (platform === 'win32') {
const result = await runCommand('for /f "delims=" %a in (\'curl -s --ssl-reqd ip.codex.storage\') do @echo %a');
nat = result.trim();
} else {
nat = await runCommand('curl -s https://ip.codex.storage');
}
const command = platform === 'win32'
? `codex ^
--data-dir=datadir ^
--disc-port=${discPort} ^
--listen-addrs=/ip4/0.0.0.0/tcp/${listenPort} ^
--nat=${nat} ^
--api-cors-origin="*" ^
--bootstrap-node=spr:CiUIAhIhAiJvIcA_ZwPZ9ugVKDbmqwhJZaig5zKyLiuaicRcCGqLEgIDARo8CicAJQgCEiECIm8hwD9nA9n26BUoNuarCEllqKDnMrIuK5qJxFwIaosQ3d6esAYaCwoJBJ_f8zKRAnU6KkYwRAIgM0MvWNJL296kJ9gWvfatfmVvT-A7O2s8Mxp8l9c8EW0CIC-h-H-jBVSgFjg3Eny2u33qF7BDnWFzo7fGfZ7_qc9P`
: `codex \
--data-dir=datadir \
--disc-port=${discPort} \
--listen-addrs=/ip4/0.0.0.0/tcp/${listenPort} \
--nat=\`curl -s https://ip.codex.storage\` \
--api-cors-origin="*" \
--bootstrap-node=spr:CiUIAhIhAiJvIcA_ZwPZ9ugVKDbmqwhJZaig5zKyLiuaicRcCGqLEgIDARo8CicAJQgCEiECIm8hwD9nA9n26BUoNuarCEllqKDnMrIuK5qJxFwIaosQ3d6esAYaCwoJBJ_f8zKRAnU6KkYwRAIgM0MvWNJL296kJ9gWvfatfmVvT-A7O2s8Mxp8l9c8EW0CIC-h-H-jBVSgFjg3Eny2u33qF7BDnWFzo7fGfZ7_qc9P`;
console.log(showInfoMessage(
'🚀 Codex node is running...\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'
));
const nodeProcess = exec(command);
await new Promise(resolve => setTimeout(resolve, 5000));
try {
const response = await axios.get('http://localhost:8080/api/codex/v1/debug/info');
if (response.status === 200) {
await logToSupabase(response.data);
console.log(boxen(
chalk.cyan('We are logging some of your node\'s public data for improving the Codex experience'),
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'cyan',
title: '🔒 Privacy Notice',
titleAlignment: 'center',
dimBorder: true
}
));
}
} catch (error) {
// Silently handle any logging errors
}
await new Promise((resolve, reject) => {
nodeProcess.on('exit', (code) => {
if (code === 0) resolve();
else reject(new Error(`Node exited with code ${code}`));
});
});
if (platform === 'win32') {
console.log(showInfoMessage('Cleaning up firewall rules...'));
await runCommand('netsh advfirewall firewall delete rule name="Allow Codex (TCP-In)"');
await runCommand('netsh advfirewall firewall delete rule name="Allow Codex (UDP-In)"');
}
} catch (error) {
console.log(showErrorMessage(`Failed to run Codex: ${error.message}`));
}
await showNavigationMenu();
}
}
async function showNodeDetails(data, showNavigationMenu) {
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: 'Select information to view:',
choices: [
'1. View Connected Peers',
'2. View Node Information',
'3. Back to Main Menu',
'4. Exit'
],
pageSize: 4,
loop: true
}
]);
switch (choice.split('.')[0].trim()) {
case '1':
const peerCount = data.table.nodes.length;
if (peerCount > 0) {
console.log(showInfoMessage('Connected Peers'));
data.table.nodes.forEach((node, index) => {
console.log(boxen(
`Peer ${index + 1}:\n` +
`${chalk.cyan('Peer ID:')} ${node.peerId}\n` +
`${chalk.cyan('Address:')} ${node.address}\n` +
`${chalk.cyan('Status:')} ${node.seen ? chalk.green('Active') : chalk.gray('Inactive')}`,
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'blue'
}
));
});
} else {
console.log(showInfoMessage('No connected peers found.'));
}
return showNodeDetails(data, showNavigationMenu);
case '2':
console.log(boxen(
`${chalk.cyan('Version:')} ${data.codex.version}\n` +
`${chalk.cyan('Revision:')} ${data.codex.revision}\n\n` +
`${chalk.cyan('Node ID:')} ${data.table.localNode.nodeId}\n` +
`${chalk.cyan('Peer ID:')} ${data.table.localNode.peerId}\n` +
`${chalk.cyan('Listening Address:')} ${data.table.localNode.address}\n\n` +
`${chalk.cyan('Public IP:')} ${data.announceAddresses[0].split('/')[2]}\n` +
`${chalk.cyan('Port:')} ${data.announceAddresses[0].split('/')[4]}\n` +
`${chalk.cyan('Connected Peers:')} ${data.table.nodes.length}`,
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'yellow',
title: '📊 Node Information',
titleAlignment: 'center'
}
));
return showNodeDetails(data, showNavigationMenu);
case '3':
return showNavigationMenu();
case '4':
process.exit(0);
}
}
export async function checkNodeStatus(showNavigationMenu) {
try {
const nodeRunning = await isNodeRunning();
if (nodeRunning) {
const spinner = createSpinner('Checking node status...').start();
const response = await runCommand('curl http://localhost:8080/api/codex/v1/debug/info -w \'\\n\'');
spinner.success();
const data = JSON.parse(response);
const peerCount = data.table.nodes.length;
const isOnline = peerCount > 2;
console.log(boxen(
isOnline
? chalk.green('Node is ONLINE & DISCOVERABLE')
: chalk.yellow('Node is ONLINE but has few peers'),
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: isOnline ? 'green' : 'yellow',
title: '🔌 Node Status',
titleAlignment: 'center'
}
));
await showNodeDetails(data, showNavigationMenu);
} else {
console.log(showErrorMessage('Codex node is not running. Try again after starting the node'));
await showNavigationMenu();
}
} catch (error) {
console.log(showErrorMessage(`Failed to check node status: ${error.message}`));
await showNavigationMenu();
}
}

134
src/main.js Normal file
View File

@ -0,0 +1,134 @@
#!/usr/bin/env node
import inquirer from 'inquirer';
import chalk from 'chalk';
import boxen from 'boxen';
import { ASCII_ART } from './constants/ascii.js';
import { handleCommandLineOperation, parseCommandLineArgs } from './cli/commandParser.js';
import { uploadFile, downloadFile, showLocalFiles } from './handlers/fileHandlers.js';
import { checkCodexInstallation, installCodex, uninstallCodex } from './handlers/installationHandlers.js';
import { runCodex, checkNodeStatus } from './handlers/nodeHandlers.js';
async function showNavigationMenu() {
console.log('\n')
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: 'What would you like to do?',
choices: [
'1. Back to main menu',
'2. Exit'
],
pageSize: 2,
loop: true
}
]);
switch (choice.split('.')[0]) {
case '1':
return main();
case '2':
handleExit();
}
}
function handleExit() {
console.log(boxen(
chalk.cyanBright('👋 Thank you for using Codex Storage CLI! Goodbye!'),
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'cyan',
title: '👋 GOODBYE',
titleAlignment: 'center'
}
));
process.exit(0);
}
export async function main() {
const commandArgs = parseCommandLineArgs();
if (commandArgs) {
switch (commandArgs.command) {
case 'upload':
await uploadFile(commandArgs.value, handleCommandLineOperation, showNavigationMenu);
return;
case 'download':
await downloadFile(commandArgs.value, handleCommandLineOperation, showNavigationMenu);
return;
}
}
process.on('SIGINT', handleExit);
process.on('SIGTERM', handleExit);
process.on('SIGQUIT', handleExit);
try {
while (true) {
console.log('\n' + chalk.cyanBright(ASCII_ART));
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: 'Select an option:',
choices: [
'1. Download and install Codex',
'2. Run Codex node',
'3. Check node status',
'4. Upload a file',
'5. Download a file',
'6. Show local data',
'7. Uninstall Codex node',
'8. Exit'
],
pageSize: 8,
loop: true
}
]).catch(() => {
handleExit();
return { choice: '8' };
});
if (choice.startsWith('8')) {
handleExit();
break;
}
switch (choice.split('.')[0]) {
case '1':
await checkCodexInstallation(showNavigationMenu);
break;
case '2':
await runCodex(showNavigationMenu);
return;
case '3':
await checkNodeStatus(showNavigationMenu);
break;
case '4':
await uploadFile(null, handleCommandLineOperation, showNavigationMenu);
break;
case '5':
await downloadFile(null, handleCommandLineOperation, showNavigationMenu);
break;
case '6':
await showLocalFiles(showNavigationMenu);
break;
case '7':
await uninstallCodex(showNavigationMenu);
break;
}
console.log('\n');
}
} catch (error) {
if (error.message.includes('ExitPromptError')) {
handleExit();
} else {
console.error(chalk.red('An error occurred:', error.message));
handleExit();
}
}
}

View File

@ -0,0 +1,73 @@
import axios from 'axios';
import { runCommand } from '../utils/command.js';
import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js';
import os from 'os';
const platform = os.platform();
export async function isNodeRunning() {
try {
const response = await axios.get('http://localhost:8080/api/codex/v1/debug/info');
return response.status === 200;
} catch (error) {
return false;
}
}
export async function isCodexInstalled() {
try {
await runCommand('codex --version');
return true;
} catch (error) {
return false;
}
}
export async function logToSupabase(nodeData) {
try {
const peerCount = nodeData.table.nodes ? nodeData.table.nodes.length : "0";
const payload = {
nodeId: nodeData.table.localNode.nodeId,
peerId: nodeData.table.localNode.peerId,
publicIp: nodeData.announceAddresses[0].split('/')[2],
version: nodeData.codex.version,
peerCount: peerCount == 0 ? "0" : peerCount,
port: nodeData.announceAddresses[0].split('/')[4],
listeningAddress: nodeData.table.localNode.address
};
const response = await axios.post('https://vfcnsjxahocmzefhckfz.supabase.co/functions/v1/codexnodes', payload, {
headers: {
'Content-Type': 'application/json'
}
});
return response.status === 200;
} catch (error) {
console.error('Failed to log to Supabase:', error.message);
if (error.response) {
console.error('Error response:', {
status: error.response.status,
data: error.response.data
});
}
return false;
}
}
export async function checkDependencies() {
if (platform === 'linux') {
try {
await runCommand('ldconfig -p | grep libgomp');
return true;
} catch (error) {
console.log(showErrorMessage('Required dependency libgomp1 is not installed.'));
console.log(showInfoMessage(
'For Debian-based Linux systems, please install it manually using:\n\n' +
'sudo apt update && sudo apt install libgomp1'
));
return false;
}
}
return true;
}

14
src/utils/command.js Normal file
View File

@ -0,0 +1,14 @@
import { exec } from 'child_process';
import { promisify } from 'util';
export const execAsync = promisify(exec);
export async function runCommand(command) {
try {
const { stdout, stderr } = await execAsync(command);
return stdout;
} catch (error) {
console.error('Error:', error.message);
throw error;
}
}

35
src/utils/messages.js Normal file
View File

@ -0,0 +1,35 @@
import boxen from 'boxen';
import chalk from 'chalk';
export function showSuccessMessage(message) {
return boxen(chalk.green(message), {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'green',
title: '✅ SUCCESS',
titleAlignment: 'center'
});
}
export function showErrorMessage(message) {
return boxen(chalk.red(message), {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'red',
title: '❌ ERROR',
titleAlignment: 'center'
});
}
export function showInfoMessage(message) {
return boxen(chalk.cyan(message), {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'cyan',
title: ' INFO',
titleAlignment: 'center'
});
}