snes-disassembler
Version:
A Super Nintendo (SNES) ROM disassembler for 65816 assembly
762 lines • 34.4 kB
JavaScript
#!/usr/bin/env node
"use strict";
/**
* SNES Disassembler Command Line Interface
*
* Usage: snes-disasm [options] <rom-file>
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runCLI = main;
const commander_1 = require("commander");
const disassembly_handler_1 = require("./disassembly-handler");
const prompts_1 = require("@clack/prompts");
const listr2_1 = require("listr2");
const chalk_1 = __importDefault(require("chalk"));
const session_manager_1 = require("./cli/session-manager");
const help_system_1 = require("./cli/help-system");
const preferences_manager_1 = require("./cli/preferences-manager");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const runInteractiveMode = async () => {
await session_manager_1.sessionManager.load();
const displayHelp = (context) => {
console.log(chalk_1.default.gray('\n--- HELP: ' + context + ' ---\n'));
console.log(chalk_1.default.dim((0, help_system_1.getHelpForContext)(context)));
};
const operationChoices = async () => {
(0, prompts_1.note)('Choose your main operation from the list below. You can select multiple steps in Follow-up.', 'Operation Selection');
const mainOperation = await (0, prompts_1.select)({
message: 'What would you like to do with your ROM?',
options: [
{ value: 'disassemble', label: '🔧 Disassemble ROM', hint: 'Convert ROM to assembly code' },
{ value: 'extract-assets', label: '🎨 Extract Assets', hint: 'Extract graphics, audio, and text' },
{ value: 'comprehensive', label: '🚀 Comprehensive Analysis', hint: 'Disassemble + extract assets + analysis' },
{ value: 'brr-decode', label: '🎵 Decode BRR Audio', hint: 'Convert BRR audio files to WAV' },
{ value: 'analysis-only', label: '📊 Analysis Only', hint: 'Advanced ROM analysis without disassembly' }
]
});
if ((0, prompts_1.isCancel)(mainOperation)) {
return null;
}
let operations = [mainOperation];
let assetTypes = [];
let analysisTypes = [];
// If user selected asset extraction or comprehensive, ask for asset types
if (mainOperation === 'extract-assets' || mainOperation === 'comprehensive') {
const selectedAssetTypes = await (0, prompts_1.multiselect)({
message: 'Which assets would you like to extract?',
options: [
{ value: 'graphics', label: '🎨 Graphics', hint: 'Sprites, backgrounds, tiles' },
{ value: 'audio', label: '🎵 Audio', hint: 'Music and sound effects' },
{ value: 'text', label: '📝 Text', hint: 'Dialogue and strings' }
],
required: true
});
if ((0, prompts_1.isCancel)(selectedAssetTypes)) {
return null;
}
assetTypes = selectedAssetTypes;
}
// If user selected analysis or comprehensive, ask for analysis types
if (mainOperation === 'analysis-only' || mainOperation === 'comprehensive') {
const selectedAnalysisTypes = await (0, prompts_1.multiselect)({
message: 'What type of analysis would you like to perform?',
options: [
{ value: 'functions', label: '📊 Function Analysis', hint: 'Detect and analyze functions' },
{ value: 'data-structures', label: '📋 Data Structure Analysis', hint: 'Identify data patterns' },
{ value: 'cross-references', label: '🔗 Cross References', hint: 'Track code relationships' },
{ value: 'quality-report', label: '📈 Quality Report', hint: 'Generate code quality metrics' },
{ value: 'ai-patterns', label: '🤖 AI Pattern Recognition', hint: 'Use AI for pattern detection' }
],
required: false
});
if ((0, prompts_1.isCancel)(selectedAnalysisTypes)) {
return null;
}
analysisTypes = selectedAnalysisTypes;
// Convert analysis-only to analysis for internal processing
if (mainOperation === 'analysis-only') {
operations = ['analysis'];
}
}
// If comprehensive, expand to individual operations
if (mainOperation === 'comprehensive') {
operations = ['disassemble', 'extract-assets'];
if (analysisTypes.length > 0) {
operations.push('analysis');
}
}
return {
operations,
assetTypes,
analysisTypes
};
};
const analyzeROM = async (filePath) => {
// Simulate intelligent analysis based on file metadata
console.log(chalk_1.default.dim(`Analyzing ROM: ${filePath}`));
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log(chalk_1.default.green('Analysis complete! Suggested output format: CA65'));
// Return mocked recommendations
return {
suggestedFormat: 'ca65',
detectedRegions: [{ start: 0x8000, end: 0xFFFF }],
};
};
(0, prompts_1.intro)(chalk_1.default.bgCyan.black(' 🎮 Welcome to the SNES Disassembler Interactive CLI 🎮 '));
console.log(chalk_1.default.gray('This CLI helps you disassemble SNES ROMs and extract assets with AI-enhanced tools.'));
(0, prompts_1.note)('Load a previous session or start a new one to begin.');
try {
const runOperations = async (operations, romFilePath, assetTypes, analysisTypes) => {
for (const op of operations) {
switch (op) {
case 'disassemble':
await handleDisassemblyWorkflow(romFilePath);
break;
case 'extract-assets':
await handleAssetExtractionWorkflow(romFilePath, assetTypes);
break;
case 'brr-decode':
await handleBRRDecodingWorkflow();
break;
case 'analysis':
await handleAnalysisWorkflow(romFilePath, analysisTypes);
break;
}
}
};
// Main action selection - ROM file selection
const recentFiles = session_manager_1.sessionManager.getRecentFiles();
let romFilePath;
if (recentFiles.length > 0) {
const useRecent = await (0, prompts_1.confirm)({
message: `Use recent ROM file: ${recentFiles[0].name}?`
});
if (useRecent && !(0, prompts_1.isCancel)(useRecent)) {
romFilePath = recentFiles[0].path;
}
else {
const romFile = await (0, prompts_1.text)({
message: 'Enter the path to your SNES ROM file:',
placeholder: './example.smc',
validate: (value) => {
if (!value)
return 'ROM file path is required';
if (!fs.existsSync(value))
return 'File does not exist';
const ext = path.extname(value).toLowerCase();
if (!['.smc', '.sfc', '.fig'].includes(ext)) {
return 'Please select a valid SNES ROM file (.smc, .sfc, .fig)';
}
return;
}
});
if ((0, prompts_1.isCancel)(romFile)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
romFilePath = romFile;
}
}
else {
const romFile = await (0, prompts_1.text)({
message: 'Enter the path to your SNES ROM file:',
placeholder: './example.smc',
validate: (value) => {
if (!value)
return 'ROM file path is required';
if (!fs.existsSync(value))
return 'File does not exist';
const ext = path.extname(value).toLowerCase();
if (!['.smc', '.sfc', '.fig'].includes(ext)) {
return 'Please select a valid SNES ROM file (.smc, .sfc, .fig)';
}
return;
}
});
if ((0, prompts_1.isCancel)(romFile)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
romFilePath = romFile;
}
// Store the selected ROM file
session_manager_1.sessionManager.setCurrentROM(romFilePath);
await session_manager_1.sessionManager.addRecentFile(romFilePath);
const selectedOperations = await operationChoices();
if (!selectedOperations || (0, prompts_1.isCancel)(selectedOperations)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
const { operations, assetTypes, analysisTypes } = selectedOperations;
if (operations.length === 0) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
(0, prompts_1.note)('Summary of selected operations:');
console.log(`Operations: ${operations.join(', ')}`);
console.log(`Assets: ${assetTypes?.join(', ') || 'N/A'}`);
console.log(`Analysis: ${analysisTypes?.join(', ') || 'N/A'}`);
const preferences = session_manager_1.sessionManager.getPreferences();
const confirmActions = preferences.confirmActions !== false;
if (!confirmActions || await (0, prompts_1.confirm)({
message: 'Proceed with these operations?'
})) {
await runOperations(operations, romFilePath, assetTypes, analysisTypes);
}
else {
(0, prompts_1.cancel)('Operation cancelled by user.');
}
}
catch (error) {
if ((0, prompts_1.isCancel)(error)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
throw error;
}
};
const handleDisassemblyWorkflow = async (romFilePath) => {
const romFile = romFilePath;
// Define analyzeROM function locally
const analyzeROM = async (filePath) => {
console.log(chalk_1.default.dim(`Analyzing ROM: ${filePath}`));
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log(chalk_1.default.green('Analysis complete! Suggested output format: CA65'));
return {
suggestedFormat: 'ca65',
detectedRegions: [{ start: 0x8000, end: 0xFFFF }],
};
};
// Intelligent ROM analysis
const analysisResult = await analyzeROM(romFile);
// Output format selection
const format = await (0, prompts_1.select)({
message: 'Select output format:',
options: [
{ value: 'ca65', label: 'CA65 Assembly', hint: 'Compatible with cc65 assembler' },
{ value: 'wla-dx', label: 'WLA-DX Assembly', hint: 'Compatible with WLA-DX assembler' },
{ value: 'bass', label: 'BASS Assembly', hint: 'Compatible with BASS assembler' },
{ value: 'html', label: 'HTML Report', hint: 'Interactive HTML documentation' },
{ value: 'json', label: 'JSON Data', hint: 'Machine-readable JSON format' },
{ value: 'markdown', label: 'Markdown Documentation', hint: 'Human-readable documentation' }
]
});
if ((0, prompts_1.isCancel)(format)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
// Advanced options
const advancedOptions = await (0, prompts_1.multiselect)({
message: 'Select advanced options (use space to select):',
options: [
{ value: 'analysis', label: 'Full Analysis', hint: 'Detect functions and data structures' },
{ value: 'enhanced-disasm', label: 'Enhanced Disassembly', hint: 'Use MCP server insights' },
{ value: 'bank-aware', label: 'Bank-Aware Addressing', hint: '24-bit addressing mode' },
{ value: 'detect-functions', label: 'Function Detection', hint: 'Automatically detect functions' },
{ value: 'generate-docs', label: 'Generate Documentation', hint: 'Create comprehensive docs' },
{ value: 'extract-assets', label: 'Extract Assets', hint: 'Also extract graphics/audio' }
],
required: false
});
if ((0, prompts_1.isCancel)(advancedOptions)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
// Address range (optional)
const useAddressRange = await (0, prompts_1.confirm)({
message: 'Do you want to specify a custom address range?'
});
let startAddress, endAddress;
if (useAddressRange && !(0, prompts_1.isCancel)(useAddressRange)) {
startAddress = await (0, prompts_1.text)({
message: 'Start address (hex, e.g., 8000):',
placeholder: '8000',
validate: (value) => {
if (value && !/^[0-9A-Fa-f]+$/.test(value)) {
return 'Please enter a valid hexadecimal address';
}
return;
}
});
if (!(0, prompts_1.isCancel)(startAddress) && startAddress) {
endAddress = await (0, prompts_1.text)({
message: 'End address (hex, e.g., FFFF):',
placeholder: 'FFFF',
validate: (value) => {
if (value && !/^[0-9A-Fa-f]+$/.test(value)) {
return 'Please enter a valid hexadecimal address';
}
return;
}
});
}
}
// Output directory - show global default if set
const globalOutputDir = session_manager_1.sessionManager.getGlobalOutputDir();
const defaultOutputDir = globalOutputDir || './output';
const placeholder = globalOutputDir ? `${globalOutputDir} (global default)` : './output';
const outputDir = await (0, prompts_1.text)({
message: 'Output directory:',
placeholder: placeholder,
defaultValue: defaultOutputDir
});
if ((0, prompts_1.isCancel)(outputDir)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
// Build CLI options
// Automatically suggest settings based on analysis
const suggestedFormat = analysisResult.suggestedFormat || format;
const advancedOptionsArray = advancedOptions;
const options = {
format: format,
outputDir: outputDir,
verbose: true,
analysis: advancedOptionsArray.includes('analysis'),
enhancedDisasm: advancedOptionsArray.includes('enhanced-disasm'),
bankAware: advancedOptionsArray.includes('bank-aware'),
detectFunctions: advancedOptionsArray.includes('detect-functions'),
generateDocs: advancedOptionsArray.includes('generate-docs'),
extractAssets: advancedOptionsArray.includes('extract-assets'),
start: startAddress,
end: endAddress
};
// Execute disassembly with progress tracking
const s = (0, prompts_1.spinner)();
s.start('Processing ROM file...');
const tasks = new listr2_1.Listr([
{
title: 'Analyzing ROM file before processing',
task: async () => {
await analyzeROM(romFile);
}
},
{
title: 'Analyzing ROM structure',
task: async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
}
},
{
title: 'Disassembling code',
task: async () => {
await (0, disassembly_handler_1.disassembleROM)(romFile, options);
}
},
{
title: 'Generating output files',
task: async () => {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
], {
rendererOptions: {
collapseSubtasks: false,
suffixSkips: true
}
});
s.stop('ROM analysis complete!');
try {
await tasks.run();
(0, prompts_1.note)(`🎉 Disassembly completed successfully!\n\n` +
`Output format: ${chalk_1.default.cyan(format)}\n` +
`Output directory: ${chalk_1.default.cyan(outputDir)}\n` +
`ROM file: ${chalk_1.default.cyan(romFile)}`, 'Success');
(0, prompts_1.outro)(chalk_1.default.green('All done! Check your output directory for the results.'));
}
catch (error) {
(0, prompts_1.outro)(chalk_1.default.red(`Error during disassembly: ${error instanceof Error ? error.message : error}`));
}
};
const handleAssetExtractionWorkflow = async (romFilePath, preSelectedAssetTypes = []) => {
const romFile = romFilePath;
let assetTypes;
// Use pre-selected asset types if available, otherwise prompt user
if (preSelectedAssetTypes.length > 0) {
assetTypes = preSelectedAssetTypes;
console.log(chalk_1.default.cyan(`Using pre-selected asset types: ${assetTypes.join(', ')}`));
}
else {
const selectedAssetTypes = await (0, prompts_1.multiselect)({
message: 'Select asset types to extract:',
options: [
{ value: 'graphics', label: '🎨 Graphics', hint: 'Sprites, backgrounds, tiles' },
{ value: 'audio', label: '🎵 Audio', hint: 'Music and sound effects' },
{ value: 'text', label: '📝 Text', hint: 'Dialogue and strings' }
],
required: true
});
if ((0, prompts_1.isCancel)(selectedAssetTypes)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
assetTypes = selectedAssetTypes;
}
// Use global output directory as base if set
const globalOutputDir = session_manager_1.sessionManager.getGlobalOutputDir();
const defaultAssetDir = globalOutputDir ? path.join(globalOutputDir, 'assets') : './assets';
const placeholder = globalOutputDir ? `${defaultAssetDir} (using global default)` : './assets';
const outputDir = await (0, prompts_1.text)({
message: 'Output directory for extracted assets:',
placeholder: placeholder,
defaultValue: defaultAssetDir
});
if ((0, prompts_1.isCancel)(outputDir)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
const assetTypesArray = assetTypes;
const options = {
extractAssets: true,
assetTypes: assetTypesArray.join(','),
outputDir: outputDir,
verbose: true
};
try {
await (0, disassembly_handler_1.disassembleROM)(romFile, options);
(0, prompts_1.outro)(chalk_1.default.green('Asset extraction completed successfully!'));
}
catch (error) {
(0, prompts_1.outro)(chalk_1.default.red(`Error during asset extraction: ${error instanceof Error ? error.message : error}`));
}
};
const handleBRRDecodingWorkflow = async () => {
const brrFile = await (0, prompts_1.text)({
message: 'Enter the path to your BRR audio file:',
placeholder: './audio.brr',
validate: (value) => {
if (!value)
return 'BRR file path is required';
if (!fs.existsSync(value))
return 'File does not exist';
return;
}
});
if ((0, prompts_1.isCancel)(brrFile)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
const outputFile = await (0, prompts_1.text)({
message: 'Output WAV file path:',
placeholder: './audio.wav',
defaultValue: path.basename(brrFile, '.brr') + '.wav'
});
if ((0, prompts_1.isCancel)(outputFile)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
const sampleRate = await (0, prompts_1.text)({
message: 'Sample rate (Hz):',
placeholder: '32000',
defaultValue: '32000',
validate: (value) => {
if (value && (isNaN(parseInt(value)) || parseInt(value) < 1000)) {
return 'Please enter a valid sample rate (minimum 1000 Hz)';
}
return;
}
});
if ((0, prompts_1.isCancel)(sampleRate)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
const enableLooping = await (0, prompts_1.confirm)({
message: 'Enable BRR loop processing?'
});
if ((0, prompts_1.isCancel)(enableLooping)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
const options = {
decodeBrr: brrFile,
brrOutput: outputFile,
brrSampleRate: sampleRate,
brrEnableLooping: enableLooping,
verbose: true
};
try {
await (0, disassembly_handler_1.disassembleROM)('', options); // Empty ROM file for BRR-only processing
(0, prompts_1.outro)(chalk_1.default.green('BRR decoding completed successfully!'));
}
catch (error) {
(0, prompts_1.outro)(chalk_1.default.red(`Error during BRR decoding: ${error instanceof Error ? error.message : error}`));
}
};
const handleAnalysisWorkflow = async (romFilePath, preSelectedAnalysisTypes = []) => {
const romFile = romFilePath;
let analysisTypes;
// Use pre-selected analysis types if available, otherwise prompt user
if (preSelectedAnalysisTypes.length > 0) {
analysisTypes = preSelectedAnalysisTypes;
console.log(chalk_1.default.cyan(`Using pre-selected analysis types: ${analysisTypes.join(', ')}`));
}
else {
const selectedAnalysisTypes = await (0, prompts_1.multiselect)({
message: 'Select analysis options:',
options: [
{ value: 'functions', label: '📊 Function Analysis', hint: 'Detect and analyze functions' },
{ value: 'data-structures', label: '📊 Data Structure Analysis', hint: 'Identify data patterns' },
{ value: 'cross-references', label: '🔗 Cross References', hint: 'Track code relationships' },
{ value: 'quality-report', label: '📈 Quality Report', hint: 'Generate code quality metrics' },
{ value: 'ai-patterns', label: '🤖 AI Pattern Recognition', hint: 'Use AI for pattern detection' }
],
required: true
});
if ((0, prompts_1.isCancel)(selectedAnalysisTypes)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
analysisTypes = selectedAnalysisTypes;
}
// Use global output directory as base if set
const globalOutputDir = session_manager_1.sessionManager.getGlobalOutputDir();
const defaultAnalysisDir = globalOutputDir ? path.join(globalOutputDir, 'analysis') : './analysis';
const placeholder = globalOutputDir ? `${defaultAnalysisDir} (using global default)` : './analysis';
const outputDir = await (0, prompts_1.text)({
message: 'Output directory for analysis results:',
placeholder: placeholder,
defaultValue: defaultAnalysisDir
});
if ((0, prompts_1.isCancel)(outputDir)) {
(0, prompts_1.cancel)('Operation cancelled.');
return;
}
const analysisTypesArray = analysisTypes;
const options = {
analysis: true,
quality: analysisTypesArray.includes('quality-report'),
enhancedDisasm: true,
detectFunctions: analysisTypesArray.includes('functions'),
generateDocs: true,
disableAI: !analysisTypesArray.includes('ai-patterns'),
outputDir: outputDir,
format: 'html', // Best format for analysis results
verbose: true
};
try {
await (0, disassembly_handler_1.disassembleROM)(romFile, options);
(0, prompts_1.outro)(chalk_1.default.green('Analysis completed successfully!'));
}
catch (error) {
(0, prompts_1.outro)(chalk_1.default.red(`Error during analysis: ${error instanceof Error ? error.message : error}`));
}
};
async function main() {
commander_1.program
.name('snes-disasm')
.description('SNES ROM Disassembler with AI-enhanced analysis capabilities')
.version('1.0.0');
// Define all commands first
commander_1.program
.command('interactive')
.alias('i')
.description('🎮 Run the interactive CLI interface')
.action(async () => {
await runInteractiveMode();
});
commander_1.program
.command('brr-to-spc <input-dir> <output-spc>')
.description('Convert BRR files to SPC format')
.action(async (inputDir, outputSPC) => {
try {
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
console.log(`Converting BRR files from ${inputDir} to ${outputSPC}...`);
const { stdout, stderr } = await execAsync(`python3 brr_to_spc.py "${inputDir}" "${outputSPC}"`);
if (stdout)
console.log(stdout);
if (stderr)
console.error(stderr);
console.log('✅ Conversion completed successfully!');
}
catch (error) {
console.error('Error during BRR to SPC conversion:', error instanceof Error ? error.message : error);
process.exit(1);
}
});
commander_1.program
.command('set-output-dir <directory>')
.description('Set the global default output directory for the session')
.action(async (directory) => {
try {
await session_manager_1.sessionManager.load();
await session_manager_1.sessionManager.setGlobalOutputDir(directory);
console.log(`Global default output directory set to: ${directory}`);
}
catch (error) {
console.error('Error:', error instanceof Error ? error.message : error);
}
});
commander_1.program
.command('clear-output-dir')
.description('Clear the global default output directory setting')
.action(async () => {
try {
await session_manager_1.sessionManager.load();
await session_manager_1.sessionManager.clearGlobalOutputDir();
console.log('Global default output directory cleared.');
}
catch (error) {
console.error('Error:', error instanceof Error ? error.message : error);
}
});
commander_1.program
.command('show-output-dir')
.description('Show the current global default output directory setting')
.action(async () => {
try {
await session_manager_1.sessionManager.load();
const globalOutputDir = session_manager_1.sessionManager.getGlobalOutputDir();
if (globalOutputDir) {
console.log(`Current global default output directory: ${globalOutputDir}`);
}
else {
console.log('No global default output directory set.');
}
}
catch (error) {
console.error('Error:', error instanceof Error ? error.message : error);
}
});
commander_1.program
.command('preferences')
.alias('prefs')
.description('⚙️ Configure user preferences for advanced options')
.action(async () => {
try {
await preferences_manager_1.preferencesManager.runPreferencesInterface();
}
catch (error) {
console.error('Error:', error instanceof Error ? error.message : error);
}
});
commander_1.program
.command('show-preferences')
.description('📋 Display current user preferences')
.action(async () => {
try {
await session_manager_1.sessionManager.load();
const summary = session_manager_1.sessionManager.getPreferencesSummary();
console.log(summary);
}
catch (error) {
console.error('Error:', error instanceof Error ? error.message : error);
}
});
// Define the main action with all options
commander_1.program
.argument('[rom-file]', 'SNES ROM file to disassemble (optional in interactive mode)')
.option('-o, --output <file>', 'Output file (default: <rom-name>.<ext>)')
.option('-d, --output-dir <directory>', 'Output directory (default: current or global directory)')
.option('-f, --format <format>', 'Output format: ca65, wla-dx, bass, html, json, xml, csv, markdown', 'ca65')
.option('-s, --start <address>', 'Start address (hex, e.g., 8000)')
.option('-e, --end <address>', 'End address (hex, e.g., FFFF)')
.option('--symbols <file>', 'Load symbol file (.sym, .mlb, .json, .csv)')
.option('--analysis', 'Enable full analysis (functions, data structures, patterns)')
.option('--quality', 'Generate code quality report')
.option('-v, --verbose', 'Verbose output')
.option('--labels <file>', 'Load custom labels file')
.option('--comments <file>', 'Load custom comments file')
.option('--extract-assets', 'Extract assets (graphics, audio, text) from ROM')
.option('--asset-types <types>', 'Asset types to extract: graphics,audio,text', 'graphics,audio,text')
.option('--asset-formats <formats>', 'Graphics formats to extract: 2bpp,4bpp,8bpp', '4bpp')
.option('--disable-ai', 'Disable AI-powered pattern recognition (AI is enabled by default)')
.option('--enhanced-disasm', 'Use enhanced disassembly algorithms with MCP server insights')
.option('--bank-aware', 'Enable bank-aware disassembly with 24-bit addressing')
.option('--detect-functions', 'Enable automatic function detection and labeling')
.option('--generate-docs', 'Generate documentation for discovered functions and data structures')
.option('--decode-brr <file>', 'Decode BRR audio file to WAV format')
.option('--brr-output <file>', 'Output file for BRR decoding (default: <brr-name>.wav)')
.option('--brr-sample-rate <rate>', 'Sample rate for BRR output (default: 32000)', '32000')
.option('--brr-enable-looping', 'Enable BRR loop processing')
.option('--brr-max-samples <count>', 'Maximum samples to decode from BRR', '1000000')
.option('--brr-info', 'Show detailed BRR file information without decoding')
.option('--brr-to-spc <input-dir> <output-spc>', 'Convert BRR files from input directory to a single SPC file')
.option('-i, --interactive', 'Run in interactive mode')
.action(async (romFile, options) => {
// Load session data first
await session_manager_1.sessionManager.load();
// Only run interactive mode if explicitly requested
if (options.interactive) {
await runInteractiveMode();
return;
}
// If no ROM file is provided and not in interactive mode, show error and usage
if (!romFile) {
console.error('Error: ROM file is required when not running in interactive mode.');
console.error('');
console.error('Options:');
console.error(' 1. Provide a ROM file: snes-disasm /path/to/rom.sfc');
console.error(' 2. Use interactive mode: snes-disasm --interactive');
console.error(' 3. Use the interactive command: snes-disasm interactive');
console.error('');
console.error('For more help, run: snes-disasm --help');
process.exit(1);
}
try {
const effectiveOutputDir = session_manager_1.sessionManager.getEffectiveOutputDir(options.outputDir);
const updatedOptions = { ...options, outputDir: effectiveOutputDir };
await (0, disassembly_handler_1.disassembleROM)(romFile, updatedOptions);
}
catch (error) {
console.error('Error:', error instanceof Error ? error.message : error);
process.exit(1);
}
});
commander_1.program.parse();
}
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
process.exit(1);
});
if (require.main === module) {
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
}
//# sourceMappingURL=cli.js.map