UNPKG

cs2-inspect-lib

Version:

Enhanced CS2 Inspect URL library with full protobuf support, validation, and error handling

447 lines 17.4 kB
#!/usr/bin/env node "use strict"; /** * CS2 Inspect URL CLI Tool * * Command-line interface for encoding and decoding CS2 inspect URLs */ Object.defineProperty(exports, "__esModule", { value: true }); const commander_1 = require("commander"); const fs_1 = require("fs"); const index_1 = require("./index"); const errors_1 = require("./errors"); const types_1 = require("./types"); const program = new commander_1.Command(); // Configure CLI program .name('cs2inspect') .description('CS2 Inspect URL utilities - encode and decode inspect URLs') .version(index_1.VERSION); // Global options program .option('-v, --verbose', 'enable verbose output') .option('--no-validate', 'disable input validation') .option('--config <file>', 'load configuration from JSON file') .option('--steam-username <username>', 'Steam username for unmasked URL support') .option('--steam-password <password>', 'Steam password for unmasked URL support') .option('--enable-steam', 'enable Steam client for unmasked URLs'); /** * Decode command */ program .command('decode <url>') .description('decode an inspect URL and display item information (supports both masked and unmasked URLs)') .option('-o, --output <file>', 'output to JSON file instead of console') .option('-f, --format <format>', 'output format (json|yaml|table)', 'json') .option('--raw', 'output raw protobuf data without formatting') .option('--force-steam', 'force use of Steam client even for masked URLs') .action(async (url, options) => { try { const config = loadConfig(options.parent?.config, { validateInput: !options.parent?.noValidate, enableLogging: options.parent?.verbose, steamClient: { enabled: options.parent?.enableSteam || false, username: options.parent?.steamUsername, password: options.parent?.steamPassword } }); const cs2 = new index_1.CS2Inspect(config); if (options.parent?.verbose) { console.log('Analyzing URL...'); } // First analyze the URL const analyzed = cs2.analyzeUrl(url); if (options.parent?.verbose) { console.log('URL type:', analyzed.url_type); if (analyzed.url_type === 'unmasked') { console.log('Market ID:', analyzed.market_id); console.log('Owner ID:', analyzed.owner_id); console.log('Asset ID:', analyzed.asset_id); console.log('Class ID:', analyzed.class_id); } } let item; // Handle different URL types if (analyzed.url_type === 'masked' && !options.forceSteam) { if (options.parent?.verbose) { console.log('Decoding protobuf data...'); } item = cs2.decodeInspectUrl(url); } else if (analyzed.url_type === 'unmasked' || options.forceSteam) { if (!config.steamClient?.enabled || !config.steamClient?.username || !config.steamClient?.password) { console.error('Error: Steam credentials required for unmasked URLs'); console.log('Use --enable-steam --steam-username <username> --steam-password <password>'); process.exit(1); } if (options.parent?.verbose) { console.log('Initializing Steam client...'); } await cs2.initializeSteamClient(); if (options.parent?.verbose) { console.log('Fetching item data from Steam...'); } item = await cs2.decodeInspectUrlAsync(url); } else { console.error('Error: Invalid URL type'); process.exit(1); } let output; if (options.raw) { output = JSON.stringify(item, (_key, value) => typeof value === 'bigint' ? value.toString() : value, 2); } else { output = formatItemOutput(item, options.format); } if (options.output) { (0, fs_1.writeFileSync)(options.output, output); console.log(`Output written to ${options.output}`); } else { console.log(output); } // Disconnect Steam client if it was used if (cs2.isSteamClientReady()) { await cs2.disconnectSteamClient(); } } catch (error) { handleError(error, options.parent?.verbose); } }); /** * Encode command */ program .command('encode') .description('create an inspect URL from item data') .option('-i, --input <file>', 'read item data from JSON file') .option('-w, --weapon <weapon>', 'weapon type (name or ID)') .option('-p, --paint <paint>', 'paint index', '0') .option('-s, --seed <seed>', 'paint seed', '0') .option('-f, --float <float>', 'wear float value', '0.0') .option('-r, --rarity <rarity>', 'item rarity') .option('-n, --name <name>', 'custom name tag') .option('--sticker <slot:id:wear>', 'add sticker (can be used multiple times)', collect, []) .option('--keychain <id:pattern>', 'add keychain') .option('-o, --output <file>', 'output URL to file instead of console') .action(async (options) => { try { const config = loadConfig(options.parent?.config, { validateInput: !options.parent?.noValidate, enableLogging: options.parent?.verbose }); const cs2 = new index_1.CS2Inspect(config); let item; if (options.input) { // Load from file if (options.parent?.verbose) { console.log(`Loading item data from ${options.input}...`); } const data = (0, fs_1.readFileSync)(options.input, 'utf8'); item = JSON.parse(data, (key, value) => { // Handle BigInt values if (key === 'itemid' && typeof value === 'string' && /^\d+$/.test(value)) { return BigInt(value); } return value; }); } else { // Build from command line options if (!options.weapon) { console.error('Error: weapon is required when not using input file'); process.exit(1); } item = buildItemFromOptions(options); } if (options.parent?.verbose) { console.log('Creating inspect URL...'); console.log('Item data:', JSON.stringify(item, (_key, value) => typeof value === 'bigint' ? value.toString() : value, 2)); } const url = cs2.createInspectUrl(item); if (options.output) { (0, fs_1.writeFileSync)(options.output, url); console.log(`URL written to ${options.output}`); } else { console.log(url); } } catch (error) { handleError(error, options.parent?.verbose); } }); /** * Validate command */ program .command('validate <url>') .description('validate an inspect URL format and content') .action(async (url, options) => { try { const config = loadConfig(options.parent?.config, { validateInput: true, enableLogging: options.parent?.verbose }); const cs2 = new index_1.CS2Inspect(config); console.log('Validating URL format...'); const urlValidation = cs2.validateUrl(url); if (!urlValidation.valid) { console.log('❌ URL format validation failed:'); urlValidation.errors.forEach(error => console.log(` - ${error}`)); if (urlValidation.warnings) { console.log('⚠️ Warnings:'); urlValidation.warnings.forEach(warning => console.log(` - ${warning}`)); } process.exit(1); } console.log('✅ URL format is valid'); // Try to analyze the URL console.log('\nAnalyzing URL structure...'); const analyzed = cs2.analyzeUrl(url); console.log(`URL type: ${analyzed.url_type}`); console.log(`Quoted format: ${analyzed.is_quoted}`); if (analyzed.url_type === 'masked' && analyzed.hex_data) { console.log('\nValidating protobuf data...'); try { const item = cs2.decodeInspectUrl(url); const itemValidation = cs2.validateItem(item); if (!itemValidation.valid) { console.log('❌ Item data validation failed:'); itemValidation.errors.forEach(error => console.log(` - ${error}`)); } else { console.log('✅ Item data is valid'); } if (itemValidation.warnings) { console.log('⚠️ Warnings:'); itemValidation.warnings.forEach(warning => console.log(` - ${warning}`)); } } catch (error) { console.log('❌ Failed to decode protobuf data:', error.message); } } } catch (error) { handleError(error, options.parent?.verbose); } }); /** * Info command */ program .command('info <url>') .description('display basic information about an inspect URL') .action(async (url, options) => { try { const config = loadConfig(options.parent?.config, { validateInput: false, // Don't validate for info command enableLogging: options.parent?.verbose }); const cs2 = new index_1.CS2Inspect(config); const info = cs2.getUrlInfo(url); console.log('URL Information:'); console.log(` Type: ${info.type}`); console.log(` Quoted: ${info.isQuoted}`); console.log(` Valid format: ${info.hasValidFormat}`); if (info.estimatedSize) { console.log(` Data size: ${info.estimatedSize} characters`); } if (info.hasValidFormat && info.type !== 'invalid') { try { const analyzed = cs2.analyzeUrl(url); console.log(` Original URL: ${analyzed.original_url.substring(0, 100)}${analyzed.original_url.length > 100 ? '...' : ''}`); console.log(` Cleaned URL: ${analyzed.cleaned_url.substring(0, 100)}${analyzed.cleaned_url.length > 100 ? '...' : ''}`); if (analyzed.url_type === 'unmasked') { console.log(` Market ID: ${analyzed.market_id || 'N/A'}`); console.log(` Owner ID: ${analyzed.owner_id || 'N/A'}`); console.log(` Asset ID: ${analyzed.asset_id}`); console.log(` Class ID: ${analyzed.class_id}`); } } catch (error) { console.log(` Error analyzing URL: ${error.message}`); } } } catch (error) { handleError(error, options.parent?.verbose); } }); /** * Steam status command */ program .command('steam-status') .description('display Steam client status and configuration') .action(async (options) => { try { const config = loadConfig(options.parent?.config, { validateInput: false, enableLogging: options.parent?.verbose, steamClient: { enabled: options.parent?.enableSteam || false, username: options.parent?.steamUsername, password: options.parent?.steamPassword } }); const cs2 = new index_1.CS2Inspect(config); const stats = cs2.getSteamClientStats(); console.log('Steam Client Status:'); console.log(` Status: ${stats.status}`); console.log(` Available: ${stats.isAvailable ? '✅' : '❌'}`); console.log(` Unmasked URL Support: ${stats.unmaskedSupport ? '✅' : '❌'}`); console.log(` Queue Length: ${stats.queueLength}`); if (config.steamClient?.enabled) { console.log('\nConfiguration:'); console.log(` Username: ${config.steamClient.username ? '✅ Set' : '❌ Not set'}`); console.log(` Password: ${config.steamClient.password ? '✅ Set' : '❌ Not set'}`); console.log(` Rate Limit: ${config.steamClient.rateLimitDelay || 1500}ms`); console.log(` Max Queue Size: ${config.steamClient.maxQueueSize || 100}`); console.log(` Request Timeout: ${config.steamClient.requestTimeout || 10000}ms`); } else { console.log('\n⚠️ Steam client is disabled. Use --enable-steam to enable.'); } } catch (error) { handleError(error, options.parent?.verbose); } }); // Helper functions function collect(value, previous) { return previous.concat([value]); } function loadConfig(configFile, defaults = {}) { let config = defaults; if (configFile) { try { const fileConfig = JSON.parse((0, fs_1.readFileSync)(configFile, 'utf8')); config = { ...defaults, ...fileConfig }; } catch (error) { console.error(`Error loading config file: ${error.message}`); process.exit(1); } } return { ...types_1.DEFAULT_CONFIG, ...config }; } function buildItemFromOptions(options) { const item = { defindex: parseWeapon(options.weapon), paintindex: parseInt(options.paint, 10), paintseed: parseInt(options.seed, 10), paintwear: parseFloat(options.float) }; if (options.rarity) { item.rarity = parseRarity(options.rarity); } if (options.name) { item.customname = options.name; } if (options.sticker && options.sticker.length > 0) { item.stickers = options.sticker.map((s) => { const [slot, id, wear] = s.split(':'); return { slot: parseInt(slot, 10), sticker_id: parseInt(id, 10), wear: wear ? parseFloat(wear) : undefined }; }); } if (options.keychain) { const [id, pattern] = options.keychain.split(':'); item.keychains = [{ slot: 0, sticker_id: parseInt(id, 10), ...(pattern && { pattern: parseInt(pattern, 10) }) }]; } return item; } function parseWeapon(weapon) { // Try to parse as number first const num = parseInt(weapon, 10); if (!isNaN(num)) { return num; } // Try to match weapon name const weaponName = weapon.toUpperCase().replace(/[-\s]/g, '_'); if (weaponName in index_1.WeaponType) { return index_1.WeaponType[weaponName]; } throw new Error(`Unknown weapon: ${weapon}`); } function parseRarity(rarity) { // Try to parse as number first const num = parseInt(rarity, 10); if (!isNaN(num)) { return num; } // Try to match rarity name const rarityName = rarity.toUpperCase().replace(/[-\s]/g, '_'); if (rarityName in index_1.ItemRarity) { return index_1.ItemRarity[rarityName]; } throw new Error(`Unknown rarity: ${rarity}`); } function formatItemOutput(item, format) { switch (format.toLowerCase()) { case 'json': return JSON.stringify(item, (_key, value) => typeof value === 'bigint' ? value.toString() : value, 2); case 'yaml': // Simple YAML-like output return Object.entries(item) .map(([key, value]) => { if (Array.isArray(value)) { return `${key}:\n${value.map(v => ` - ${JSON.stringify(v)}`).join('\n')}`; } return `${key}: ${typeof value === 'bigint' ? value.toString() : JSON.stringify(value)}`; }) .join('\n'); case 'table': // Simple table format let output = 'Item Information:\n'; output += '='.repeat(50) + '\n'; Object.entries(item).forEach(([key, value]) => { if (!Array.isArray(value)) { const displayValue = typeof value === 'bigint' ? value.toString() : value; output += `${key.padEnd(20)}: ${displayValue}\n`; } }); if (item.stickers && item.stickers.length > 0) { output += '\nStickers:\n'; item.stickers.forEach((sticker, i) => { output += ` ${i + 1}. Slot ${sticker.slot}, ID ${sticker.sticker_id}`; if (sticker.wear !== undefined) output += `, Wear ${sticker.wear}`; output += '\n'; }); } return output; default: throw new Error(`Unknown format: ${format}`); } } function handleError(error, verbose = false) { if (error instanceof errors_1.CS2InspectError) { console.error(`Error [${error.code}]: ${error.message}`); if (verbose && error.context) { console.error('Context:', JSON.stringify(error.context, null, 2)); } } else { console.error('Error:', error.message); if (verbose) { console.error(error.stack); } } process.exit(1); } // Parse command line arguments program.parse(); //# sourceMappingURL=cli.js.map