import chalk from 'chalk'; import inquirer from 'inquirer'; import figlet from 'figlet'; import { removeFromIpfs } from './utils/removeFromIpfs'; import tracker, { getTrackErrorReason } from './utils/tracker'; import { TRACK_EVENTS, TRACK_PAGES, resolveTrackAction, } from './utils/trackerEvents'; interface RemoveOptions { [key: string]: any; } import { checkNodeVersion } from './utils/checkNodeVersion'; checkNodeVersion(); // Validate IPFS hash format function isValidIPFSHash(hash: string): boolean { // IPFS v0 hash (Qm...) const v0Pattern = /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/; // IPFS v1 hash (bafy...) const v1Pattern = /^bafy[a-z2-7]{50,}$/; // IPFS v1 hash (bafk...) const v1kPattern = /^bafk[a-z2-7]{50,}$/; // IPFS v1 hash (bafybe...) const v1bePattern = /^bafybe[a-z2-7]{50,}$/; return v0Pattern.test(hash) || v1Pattern.test(hash) || v1kPattern.test(hash) || v1bePattern.test(hash); } // Validate subname format (simple alphanumeric combination) function isValidSubname(subname: string): boolean { const subnamePattern = /^[a-zA-Z0-9]{6,12}$/; return subnamePattern.test(subname); } // Parse different input formats function parseInput(input: string): { type: 'hash' | 'subname'; value: string } | null { const trimmedInput = input.trim(); // Case 1: Full URL with hash - https://bafybeigthbkdv2ufll47r7e7f5z4c3vubyggxwotl52parmy3d3abt6ztu.pinme.eth.limo const hashUrlPattern = /https?:\/\/([a-z0-9]{50,})\.pinme\.dev/i; const hashUrlMatch = trimmedInput.match(hashUrlPattern); if (hashUrlMatch) { const hash = hashUrlMatch[1]; if (isValidIPFSHash(hash)) { return { type: 'hash', value: hash }; } } // Case 2: Direct hash - bafybeigthbkdv2ufll47r7e7f5z4c3vubyggxwotl52parmy3d3abt6ztu if (isValidIPFSHash(trimmedInput)) { return { type: 'hash', value: trimmedInput }; } // Case 3: Subname URL - https://3abt6ztu. const subnameUrlPattern = /https?:\/\/([a-zA-Z0-9]{6,12})\.[a-zA-Z0-9.-]+/i; const subnameUrlMatch = trimmedInput.match(subnameUrlPattern); if (subnameUrlMatch) { const subname = subnameUrlMatch[1]; if (isValidSubname(subname)) { return { type: 'subname', value: subname }; } } // Case 4: Direct subname - 3abt6ztu if (isValidSubname(trimmedInput)) { return { type: 'subname', value: trimmedInput }; } return null; } export default async (options?: RemoveOptions): Promise => { try { console.log( figlet.textSync('PINME', { font: 'Standard', horizontalLayout: 'default', verticalLayout: 'default', width: 180, whitespaceBreak: true, }), ); // If parameter is passed, remove directly, e.g.: pinme rm const argHash = process.argv[3]; if (argHash && !argHash.startsWith('-')) { const parsedInput = parseInput(argHash); if (!parsedInput) { console.log(chalk.red(`Invalid input format: ${argHash}`)); console.log(chalk.yellow('Supported formats:')); console.log(chalk.yellow(' - IPFS hash: bafybeig...')); console.log(chalk.yellow(' - Subname: 3abt6ztu')); console.log(chalk.yellow(' - Subname URL: https://3abt6ztu.')); return; } try { const success = await removeFromIpfs(parsedInput.value, parsedInput.type); if (success) { void tracker.trackEvent(TRACK_EVENTS.removeSuccess, TRACK_PAGES.remove, { a: resolveTrackAction(TRACK_EVENTS.removeSuccess), input_type: parsedInput.type, }); console.log( chalk.cyan( figlet.textSync('Successful', { horizontalLayout: 'full' }), ), ); } } catch (error: any) { void tracker.trackEvent(TRACK_EVENTS.removeFailed, TRACK_PAGES.remove, { a: resolveTrackAction(TRACK_EVENTS.removeFailed), input_type: parsedInput.type, reason: getTrackErrorReason(error), }); console.error(chalk.red(`Error: ${error.message}`)); } return; } // Interactive removal console.log(chalk.yellow('⚠️ Warning: This action will permanently remove the content from IPFS network')); console.log(chalk.yellow('⚠️ Make sure you have the correct IPFS hash')); console.log(''); const confirmAnswer = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: 'Do you want to continue?', default: false, }, ]); if (!confirmAnswer.confirm) { console.log(chalk.yellow('Operation cancelled')); return; } const answer = await inquirer.prompt([ { type: 'input', name: 'input', message: 'Enter IPFS hash, subname, or URL to remove:', validate: (input: string) => { if (!input.trim()) { return 'Please enter an IPFS hash, subname, or URL'; } const parsedInput = parseInput(input.trim()); if (!parsedInput) { return 'Invalid format. Supported: IPFS hash, full URL (*), subname, or subname URL (*.)'; } return true; }, }, ]); if (answer.input) { const parsedInput = parseInput(answer.input.trim()); if (!parsedInput) { console.log(chalk.red('Invalid input format')); return; } // Final confirmation const finalConfirm = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: `Are you sure you want to remove ${parsedInput.type === 'hash' ? 'hash' : 'subname'}: ${parsedInput.value}?`, default: false, }, ]); if (!finalConfirm.confirm) { console.log(chalk.yellow('Operation cancelled')); return; } try { const success = await removeFromIpfs(parsedInput.value, parsedInput.type); if (success) { void tracker.trackEvent(TRACK_EVENTS.removeSuccess, TRACK_PAGES.remove, { a: resolveTrackAction(TRACK_EVENTS.removeSuccess), input_type: parsedInput.type, }); console.log( chalk.cyan( figlet.textSync('Successful', { horizontalLayout: 'full' }), ), ); } } catch (error: any) { void tracker.trackEvent(TRACK_EVENTS.removeFailed, TRACK_PAGES.remove, { a: resolveTrackAction(TRACK_EVENTS.removeFailed), input_type: parsedInput.type, reason: getTrackErrorReason(error), }); console.error(chalk.red(`Error: ${error.message}`)); } } } catch (error: any) { void tracker.trackEvent(TRACK_EVENTS.removeFailed, TRACK_PAGES.remove, { a: resolveTrackAction(TRACK_EVENTS.removeFailed), reason: getTrackErrorReason(error), }); console.error(chalk.red(`Error executing remove command: ${error.message}`)); console.error(error.stack); } };