New Features: - Interactive Terminal Component (InteractiveTerminal.tsx) - Full command input with auto-completion - Command history navigation (up/down arrows) - Real-time suggestions - Auto-scroll and focus management - CLI Command System (cli-commands.ts) - help: Display available commands - clear/cls: Clear terminal - run/execute: Execute Lua scripts - check/lint: Syntax validation - count: Line/word/character counting - api: Roblox API documentation lookup - template: Template management - export: Export scripts to file - echo: Print text - info: System information - Enhanced ConsolePanel - New "Terminal" tab with interactive CLI - Existing log tabs (All, Roblox, Web, Mobile) - Props for code context and modification - Integrated with file system - Keyboard Shortcuts - Ctrl/Cmd + ` : Toggle terminal (VS Code style) Technical Details: - Command execution with context awareness - Auto-completion for commands - Command aliases support - Error handling and user feedback - History management with localStorage-ready structure - Integration with existing code editor
348 lines
9.2 KiB
TypeScript
348 lines
9.2 KiB
TypeScript
// CLI Command System for AeThex Studio
|
|
|
|
export interface CLICommand {
|
|
name: string;
|
|
description: string;
|
|
usage: string;
|
|
aliases?: string[];
|
|
execute: (args: string[], context: CLIContext) => Promise<CLIResult>;
|
|
}
|
|
|
|
export interface CLIContext {
|
|
currentCode: string;
|
|
currentFile?: string;
|
|
files: any[];
|
|
setCode?: (code: string) => void;
|
|
addLog?: (message: string, type?: 'log' | 'warn' | 'error' | 'info') => void;
|
|
}
|
|
|
|
export interface CLIResult {
|
|
success: boolean;
|
|
output: string;
|
|
type?: 'log' | 'warn' | 'error' | 'info';
|
|
}
|
|
|
|
// Built-in CLI commands
|
|
export const commands: Record<string, CLICommand> = {
|
|
help: {
|
|
name: 'help',
|
|
description: 'Display available commands',
|
|
usage: 'help [command]',
|
|
execute: async (args) => {
|
|
if (args.length > 0) {
|
|
const cmd = commands[args[0]] || Object.values(commands).find(c => c.aliases?.includes(args[0]));
|
|
if (cmd) {
|
|
return {
|
|
success: true,
|
|
output: `${cmd.name} - ${cmd.description}\nUsage: ${cmd.usage}${cmd.aliases ? `\nAliases: ${cmd.aliases.join(', ')}` : ''}`,
|
|
type: 'info',
|
|
};
|
|
}
|
|
return {
|
|
success: false,
|
|
output: `Command not found: ${args[0]}`,
|
|
type: 'error',
|
|
};
|
|
}
|
|
|
|
const commandList = Object.values(commands)
|
|
.map(cmd => ` ${cmd.name.padEnd(15)} - ${cmd.description}`)
|
|
.join('\n');
|
|
|
|
return {
|
|
success: true,
|
|
output: `Available Commands:\n${commandList}\n\nType 'help <command>' for more info`,
|
|
type: 'info',
|
|
};
|
|
},
|
|
},
|
|
|
|
clear: {
|
|
name: 'clear',
|
|
description: 'Clear the terminal',
|
|
usage: 'clear',
|
|
aliases: ['cls'],
|
|
execute: async () => {
|
|
return {
|
|
success: true,
|
|
output: '__CLEAR__',
|
|
type: 'info',
|
|
};
|
|
},
|
|
},
|
|
|
|
run: {
|
|
name: 'run',
|
|
description: 'Execute current Lua script',
|
|
usage: 'run',
|
|
aliases: ['execute', 'exec'],
|
|
execute: async (args, context) => {
|
|
if (!context.currentCode || context.currentCode.trim() === '') {
|
|
return {
|
|
success: false,
|
|
output: 'No code to execute',
|
|
type: 'error',
|
|
};
|
|
}
|
|
|
|
try {
|
|
// Simulate script execution
|
|
return {
|
|
success: true,
|
|
output: `Executing script...\n${context.currentFile || 'Untitled'}\n\nScript executed successfully (simulated)`,
|
|
type: 'info',
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
output: `Execution failed: ${error}`,
|
|
type: 'error',
|
|
};
|
|
}
|
|
},
|
|
},
|
|
|
|
check: {
|
|
name: 'check',
|
|
description: 'Check current script for syntax errors',
|
|
usage: 'check',
|
|
aliases: ['lint', 'validate'],
|
|
execute: async (args, context) => {
|
|
if (!context.currentCode || context.currentCode.trim() === '') {
|
|
return {
|
|
success: false,
|
|
output: 'No code to check',
|
|
type: 'warn',
|
|
};
|
|
}
|
|
|
|
// Simple Lua syntax checks
|
|
const issues: string[] = [];
|
|
const lines = context.currentCode.split('\n');
|
|
|
|
lines.forEach((line, i) => {
|
|
// Check for common syntax issues
|
|
if (line.includes('end)') && !line.includes('function')) {
|
|
issues.push(`Line ${i + 1}: Possible syntax error - 'end)' found`);
|
|
}
|
|
if ((line.match(/\(/g) || []).length !== (line.match(/\)/g) || []).length) {
|
|
issues.push(`Line ${i + 1}: Unbalanced parentheses`);
|
|
}
|
|
});
|
|
|
|
if (issues.length === 0) {
|
|
return {
|
|
success: true,
|
|
output: `✓ No syntax errors found (${lines.length} lines checked)`,
|
|
type: 'info',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
output: `Found ${issues.length} potential issue(s):\n${issues.join('\n')}`,
|
|
type: 'warn',
|
|
};
|
|
},
|
|
},
|
|
|
|
count: {
|
|
name: 'count',
|
|
description: 'Count lines, words, or characters in current script',
|
|
usage: 'count [lines|words|chars]',
|
|
execute: async (args, context) => {
|
|
if (!context.currentCode) {
|
|
return {
|
|
success: false,
|
|
output: 'No code to count',
|
|
type: 'error',
|
|
};
|
|
}
|
|
|
|
const lines = context.currentCode.split('\n').length;
|
|
const words = context.currentCode.split(/\s+/).filter(w => w.length > 0).length;
|
|
const chars = context.currentCode.length;
|
|
|
|
const type = args[0]?.toLowerCase();
|
|
|
|
switch (type) {
|
|
case 'lines':
|
|
return { success: true, output: `Lines: ${lines}`, type: 'info' };
|
|
case 'words':
|
|
return { success: true, output: `Words: ${words}`, type: 'info' };
|
|
case 'chars':
|
|
case 'characters':
|
|
return { success: true, output: `Characters: ${chars}`, type: 'info' };
|
|
default:
|
|
return {
|
|
success: true,
|
|
output: `Lines: ${lines} | Words: ${words} | Characters: ${chars}`,
|
|
type: 'info',
|
|
};
|
|
}
|
|
},
|
|
},
|
|
|
|
api: {
|
|
name: 'api',
|
|
description: 'Search Roblox API documentation',
|
|
usage: 'api <service|class>',
|
|
execute: async (args) => {
|
|
if (args.length === 0) {
|
|
return {
|
|
success: false,
|
|
output: 'Usage: api <service|class>\nExample: api Players, api Workspace',
|
|
type: 'warn',
|
|
};
|
|
}
|
|
|
|
const query = args.join(' ');
|
|
const commonAPIs = {
|
|
'Players': 'Service for managing player instances',
|
|
'Workspace': 'Container for 3D objects in the game',
|
|
'ReplicatedStorage': 'Storage for replicated objects',
|
|
'ServerStorage': 'Server-only storage',
|
|
'Instance': 'Base class for all Roblox objects',
|
|
'Part': 'Basic building block',
|
|
'Script': 'Server-side Lua code',
|
|
'LocalScript': 'Client-side Lua code',
|
|
};
|
|
|
|
const match = Object.keys(commonAPIs).find(
|
|
key => key.toLowerCase() === query.toLowerCase()
|
|
);
|
|
|
|
if (match) {
|
|
return {
|
|
success: true,
|
|
output: `${match}: ${commonAPIs[match as keyof typeof commonAPIs]}\n\nDocs: https://create.roblox.com/docs/reference/engine/classes/${match}`,
|
|
type: 'info',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
output: `Search: https://create.roblox.com/docs/reference/engine/classes/${query}`,
|
|
type: 'info',
|
|
};
|
|
},
|
|
},
|
|
|
|
template: {
|
|
name: 'template',
|
|
description: 'List or load code templates',
|
|
usage: 'template [list|<name>]',
|
|
execute: async (args, context) => {
|
|
if (args.length === 0 || args[0] === 'list') {
|
|
return {
|
|
success: true,
|
|
output: `Available templates:
|
|
- basic : Basic Roblox script structure
|
|
- playeradded : PlayerAdded event handler
|
|
- datastore : DataStore usage example
|
|
- remote : RemoteEvent/RemoteFunction
|
|
- gui : GUI scripting basics
|
|
|
|
Usage: template <name>`,
|
|
type: 'info',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
output: `Template '${args[0]}' - Use Templates panel (Ctrl+T) to load templates into editor`,
|
|
type: 'info',
|
|
};
|
|
},
|
|
},
|
|
|
|
export: {
|
|
name: 'export',
|
|
description: 'Export current script to file',
|
|
usage: 'export [filename]',
|
|
execute: async (args, context) => {
|
|
const filename = args[0] || 'script.lua';
|
|
const code = context.currentCode || '';
|
|
|
|
try {
|
|
const blob = new Blob([code], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename.endsWith('.lua') ? filename : `${filename}.lua`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
return {
|
|
success: true,
|
|
output: `Exported to ${a.download}`,
|
|
type: 'info',
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
output: `Export failed: ${error}`,
|
|
type: 'error',
|
|
};
|
|
}
|
|
},
|
|
},
|
|
|
|
echo: {
|
|
name: 'echo',
|
|
description: 'Print text to terminal',
|
|
usage: 'echo <text>',
|
|
execute: async (args) => {
|
|
return {
|
|
success: true,
|
|
output: args.join(' '),
|
|
type: 'log',
|
|
};
|
|
},
|
|
},
|
|
|
|
info: {
|
|
name: 'info',
|
|
description: 'Display system information',
|
|
usage: 'info',
|
|
execute: async (args, context) => {
|
|
return {
|
|
success: true,
|
|
output: `AeThex Studio v1.0.0
|
|
Environment: Browser-based IDE
|
|
Platform: Roblox Lua
|
|
Files: ${context.files?.length || 0}
|
|
Current File: ${context.currentFile || 'Untitled'}
|
|
Code Size: ${context.currentCode?.length || 0} characters`,
|
|
type: 'info',
|
|
};
|
|
},
|
|
},
|
|
};
|
|
|
|
export function executeCommand(
|
|
input: string,
|
|
context: CLIContext
|
|
): Promise<CLIResult> {
|
|
const parts = input.trim().split(/\s+/);
|
|
const commandName = parts[0].toLowerCase();
|
|
const args = parts.slice(1);
|
|
|
|
// Find command by name or alias
|
|
const command = commands[commandName] ||
|
|
Object.values(commands).find(cmd =>
|
|
cmd.aliases?.includes(commandName)
|
|
);
|
|
|
|
if (!command) {
|
|
return Promise.resolve({
|
|
success: false,
|
|
output: `Command not found: ${commandName}\nType 'help' for available commands`,
|
|
type: 'error',
|
|
});
|
|
}
|
|
|
|
return command.execute(args, context);
|
|
}
|