mirror of
https://github.com/codex-storage/cli.git
synced 2025-02-28 16:40:43 +00:00
428 lines
16 KiB
JavaScript
Executable File
428 lines
16 KiB
JavaScript
Executable File
#!/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';
|
|
|
|
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 |
|
|
+--------------------------------------------------------------------+
|
|
`;
|
|
|
|
|
|
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'
|
|
]
|
|
}
|
|
]);
|
|
|
|
switch (choice.split('.')[0]) {
|
|
case '1':
|
|
return main(); // Returns to main menu
|
|
case '2':
|
|
console.log(chalk.cyanBright('\nGoodbye! 👋\n'));
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
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 installCodex() { // TODO : TEST INSTALLATION TO SEE IF BACKGROUND SHELL WORKS CORRECTLY
|
|
if (platform === 'win32') {
|
|
console.log(chalk.yellow('Coming soon for Windows!'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log(chalk.cyanBright('Downloading Codex binaries...'));
|
|
const downloadCommand = 'curl -# -L https://get.codex.storage/install.sh | bash';
|
|
await runCommand(downloadCommand);
|
|
console.log(chalk.green('Codex binaries downloaded'));
|
|
|
|
console.log(chalk.cyanBright('Installing dependencies...'));
|
|
await runCommand('sudo apt update && sudo apt install libgomp1');
|
|
console.log(chalk.green('Dependencies installed'));
|
|
|
|
const version = await runCommand('codex --version');
|
|
console.log(chalk.green('Codex is successfully installed. Version:'));
|
|
console.log(chalk.cyanBright(version));
|
|
} catch (error) {
|
|
console.error(chalk.red('Failed to install Codex:', error.message));
|
|
}
|
|
}
|
|
|
|
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 runCodex() {
|
|
// Check if a Codex node is already running
|
|
const nodeAlreadyRunning = await isNodeRunning();
|
|
|
|
if (nodeAlreadyRunning) {
|
|
console.log(chalk.green('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(chalk.cyanBright('\n\n Codex node is running...'));
|
|
console.log(chalk.cyanBright('\n Please keep this terminal open. Start a new terminal to start interacting with the node'));
|
|
console.log(chalk.cyanBright('\n Press CTRL+C to stop the node'));
|
|
await runCommand(command);
|
|
// TODO : HANDLE THIS CHECKING PART
|
|
const peerIdResponse = await runCommand('curl http://localhost:8080/api/codex/v1/peerid -w \'\\n\'');
|
|
console.log(chalk.green('Codex node is successfully running. Peer ID:'));
|
|
console.log(chalk.cyanBright(peerIdResponse.trim()));
|
|
await showNavigationMenu();
|
|
} catch (error) {
|
|
console.error(chalk.red('Failed to run Codex:', error.message));
|
|
await showNavigationMenu();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function checkNodeStatus() {
|
|
if (platform === 'win32') {
|
|
console.log(chalk.yellow('Coming soon for Windows!'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Check if a Codex node is already running
|
|
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 based on the connected peers
|
|
const peerCount = data.table.nodes.length;
|
|
const isOnline = peerCount > 2;
|
|
|
|
// Display node status based on connected peers
|
|
const statusMessage = isOnline
|
|
? chalk.bgGreen(" Node status : ONLINE & DISCOVERABLE ")
|
|
: chalk.bgRed(" Node status : OFFLINE ");
|
|
const peerMessage = `Connected peers : ${peerCount}`;
|
|
|
|
console.log('\n' + chalk.bold.cyanBright('📊 Node Status Summary'));
|
|
console.log('━'.repeat(50));
|
|
|
|
// Version Information
|
|
console.log(chalk.bold.cyanBright('🔹 Version Info'));
|
|
console.log(` Version: ${data.codex.version}`);
|
|
console.log(` Revision: ${data.codex.revision}\n`);
|
|
|
|
// Local Node Information
|
|
console.log(chalk.bold.cyanBright('🔹 Local Node'));
|
|
console.log(` Node ID: ${data.table.localNode.nodeId}`);
|
|
console.log(` Peer ID: ${data.table.localNode.peerId}`);
|
|
console.log(` Listening Address: ${data.table.localNode.address}\n`);
|
|
|
|
// Network Information
|
|
console.log(chalk.bold.cyanBright('🔹 Network Status'));
|
|
console.log(` Public IP: ${data.announceAddresses[0].split('/')[2]}`);
|
|
console.log(` Port: ${data.announceAddresses[0].split('/')[4]}\n`);
|
|
|
|
// Connected Peers Details
|
|
if (peerCount > 0) {
|
|
console.log(chalk.bold.cyanBright('🔹 Connected Peers'));
|
|
data.table.nodes.forEach((node, index) => {
|
|
console.log(` ${index + 1}. Peer ${chalk.cyan(node.peerId)}`);
|
|
console.log(` Address: ${node.address}`);
|
|
console.log(` Status: ${node.seen ? chalk.green('Active') : chalk.gray('Inactive')}`);
|
|
if (index < peerCount - 1) console.log(''); // Add spacing between peers
|
|
});
|
|
} else {
|
|
console.log(chalk.red('No connected peers.'));
|
|
}
|
|
|
|
console.log('━'.repeat(50));
|
|
await showNavigationMenu();
|
|
}
|
|
else{
|
|
console.log(chalk.red('\nOops...Codex node is not running. Try again after starting the node'));
|
|
await showNavigationMenu();
|
|
}
|
|
} catch (error) {
|
|
console.error(chalk.red('Failed to check node status:', error.message));
|
|
await showNavigationMenu();
|
|
}
|
|
}
|
|
|
|
|
|
async function uploadFile() {
|
|
if (platform === 'win32') {
|
|
console.log(chalk.yellow('Coming soon for Windows!'));
|
|
return;
|
|
}
|
|
const nodeRunning = await isNodeRunning();
|
|
|
|
if (nodeRunning) {
|
|
console.log(chalk.bgYellow('\n ⚠️ Warning: Codex does not encrypt files. Anything uploaded will be available publicly on testnet. The testnet does not provide any guarentees - please do not use in production ⚠️ \n'));
|
|
|
|
const { filePath } = await inquirer.prompt([
|
|
{
|
|
type: 'input',
|
|
name: 'filePath',
|
|
message: 'Enter the file path to upload:',
|
|
validate: input => input.length > 0
|
|
}
|
|
|
|
]);
|
|
|
|
try {
|
|
const spinner = createSpinner('Uploading file').start();
|
|
// TODO : Upload along with metadata like file name, extension etc.,
|
|
const result = await runCommand(`curl -X POST http://localhost:8080/api/codex/v1/data -H 'Content-Type: application/octet-stream' -w '\\n' -T ${filePath}`);
|
|
spinner.success();
|
|
console.log(chalk.green('Successfully uploaded!'));
|
|
console.log(chalk.bgGreen('\nCID:', result.trim()));
|
|
await showNavigationMenu();
|
|
} catch (error) {
|
|
console.error(chalk.red('Failed to upload file:', error.message));
|
|
await showNavigationMenu();
|
|
}
|
|
}
|
|
else{
|
|
console.log(chalk.red('\nOops...Codex node is not running. Try again after starting the node'));
|
|
await showNavigationMenu();
|
|
}
|
|
}
|
|
|
|
async function downloadFile() {
|
|
if (platform === 'win32') {
|
|
console.log(chalk.yellow('Coming soon for Windows!'));
|
|
return;
|
|
}
|
|
|
|
const nodeRunning = await isNodeRunning();
|
|
|
|
if (nodeRunning) {
|
|
const { cid } = await inquirer.prompt([
|
|
{
|
|
type: 'input',
|
|
name: 'cid',
|
|
message: 'Enter the CID:',
|
|
validate: input => input.length > 0
|
|
}
|
|
]);
|
|
|
|
try {
|
|
const spinner = createSpinner('Downloading file').start();
|
|
await runCommand(`curl "http://localhost:8080/api/codex/v1/data/${cid}/network/stream" -o "${cid}.png"`);
|
|
spinner.success();
|
|
console.log(chalk.green(`Successfully downloaded!`));
|
|
console.log(chalk.bgGreen(`\nFile saved as ${cid}.png`));
|
|
await showNavigationMenu();
|
|
} catch (error) {
|
|
console.error(chalk.red('Failed to download file:', error.message));
|
|
await showNavigationMenu();
|
|
}
|
|
}
|
|
else{
|
|
console.log(chalk.red('\nOops...Codex node is not running. Try again after starting the node'));
|
|
await showNavigationMenu();
|
|
}
|
|
}
|
|
|
|
async function showLocalFiles() {
|
|
if (platform === 'win32') {
|
|
console.log(chalk.yellow('Coming soon for Windows!'));
|
|
return;
|
|
}
|
|
const nodeRunning = await isNodeRunning();
|
|
|
|
if (nodeRunning) {
|
|
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(chalk.cyanBright('\nLocal Files:'));
|
|
console.log('━'.repeat(50));
|
|
|
|
// 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();
|
|
|
|
console.log(`📁 File ${index + 1}:`);
|
|
console.log(` Filename : ${chalk.green(filename)}`);
|
|
console.log(` CID : ${chalk.cyan(cid)}`);
|
|
console.log(` Root Hash : ${chalk.cyan(rootHash)}`);
|
|
console.log(` Original Bytes : ${chalk.yellow(originalBytes)}`);
|
|
console.log(` Block Size : ${chalk.yellow(blockSize)}`);
|
|
console.log(` Protected : ${chalk.yellow(isProtected ? 'Yes' : 'No')}`);
|
|
console.log(` MIME Type : ${chalk.green(mimetype)}`);
|
|
console.log(` Uploaded At : ${chalk.magenta(uploadedDate)}`);
|
|
console.log('━'.repeat(50));
|
|
});
|
|
await showNavigationMenu();
|
|
|
|
} else {
|
|
console.log(chalk.red('No local files found.'));
|
|
await showNavigationMenu();
|
|
}
|
|
} catch (error) {
|
|
console.error(chalk.red('Failed to show local files:', error.message));
|
|
await showNavigationMenu();
|
|
}
|
|
}
|
|
else{
|
|
console.log(chalk.red('\nOops...Codex node is not running. Try again after starting the node'));
|
|
await showNavigationMenu();
|
|
}
|
|
}
|
|
|
|
|
|
async function main() {
|
|
|
|
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 Codex node',
|
|
'2. Run Codex node',
|
|
'3. Check node status',
|
|
'4. Upload a file',
|
|
'5. Download a file',
|
|
'6. Show local data',
|
|
'7. Exit'
|
|
]
|
|
}
|
|
]);
|
|
|
|
if (choice.startsWith('7.')) {
|
|
console.log(chalk.cyanBright('\nGoodbye! 👋\n'));
|
|
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;
|
|
}
|
|
|
|
console.log('\n'); // Add some spacing between operations
|
|
}
|
|
}
|
|
|
|
// Run the CLI
|
|
main().catch(console.error);
|