diff --git a/.github/workflows/php81.yaml b/.github/workflows/php81.yaml index fead114..77e68ff 100644 --- a/.github/workflows/php81.yaml +++ b/.github/workflows/php81.yaml @@ -8,14 +8,14 @@ on: jobs: test: name: Run Tests - uses: WebFiori/workflows/.github/workflows/test-php.yaml@main + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 with: php-version: '8.1' code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 with: php-version: '8.1' coverage-file: 'php-8.1-coverage.xml' diff --git a/.github/workflows/php82.yaml b/.github/workflows/php82.yaml index 978033a..9e431d3 100644 --- a/.github/workflows/php82.yaml +++ b/.github/workflows/php82.yaml @@ -8,7 +8,7 @@ on: jobs: test: name: Run Tests - uses: WebFiori/workflows/.github/workflows/test-php.yaml@main + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 with: php-version: '8.2' phpunit-config: "tests/phpunit10.xml" @@ -16,7 +16,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 with: php-version: '8.2' coverage-file: 'php-8.2-coverage.xml' diff --git a/.github/workflows/php83.yaml b/.github/workflows/php83.yaml index 56bbbd1..17c1972 100644 --- a/.github/workflows/php83.yaml +++ b/.github/workflows/php83.yaml @@ -12,7 +12,7 @@ jobs: test: name: Run Tests - uses: WebFiori/workflows/.github/workflows/test-php.yaml@main + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 with: php-version: '8.3' phpunit-config: 'tests/phpunit10.xml' @@ -21,7 +21,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 with: php-version: '8.3' coverage-file: 'php-8.3-coverage.xml' diff --git a/.github/workflows/php84.yaml b/.github/workflows/php84.yaml index 1ce3097..adcc94b 100644 --- a/.github/workflows/php84.yaml +++ b/.github/workflows/php84.yaml @@ -8,7 +8,7 @@ on: jobs: test: name: Run Tests - uses: WebFiori/workflows/.github/workflows/test-php.yaml@main + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 with: php-version: '8.4' phpunit-config: "tests/phpunit10.xml" @@ -16,7 +16,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 with: php-version: '8.4' coverage-file: 'php-8.4-coverage.xml' diff --git a/.github/workflows/php85.yaml b/.github/workflows/php85.yaml new file mode 100644 index 0000000..b6743fd --- /dev/null +++ b/.github/workflows/php85.yaml @@ -0,0 +1,27 @@ +name: Build PHP 8.5 + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] +jobs: + test: + name: Run Tests + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 + with: + php-version: '8.5' + phpunit-config: "tests/phpunit10.xml" + + code-coverage: + name: Coverage + needs: test + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 + with: + php-version: '8.5' + coverage-file: 'php-8.5-coverage.xml' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + + diff --git a/README.md b/README.md index 35ffc2d..7b879ab 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ Class library that can help in writing command line based applications with minimum dependencies using PHP.
-
-
+
+
@@ -56,6 +56,7 @@ Class library that can help in writing command line based applications with mini
|
|
|
|
|
|
+|
|
## Features
* **Easy Command Creation**: Simple class-based approach to building CLI commands
diff --git a/WebFiori/Cli/Command.php b/WebFiori/Cli/Command.php
index 276f767..faafdd0 100644
--- a/WebFiori/Cli/Command.php
+++ b/WebFiori/Cli/Command.php
@@ -579,6 +579,56 @@ public function getInput(string $prompt, ?string $default = null, ?InputValidato
return null;
}
/**
+ * Reads user input with characters masked by a specified character.
+ *
+ * This method is similar to getInput() but masks the input characters as the user types,
+ * making it suitable for sensitive information like passwords, tokens, or secrets.
+ * The actual input value is captured but only mask characters are displayed in the terminal.
+ *
+ * @param string $prompt The prompt message to display to the user. Must be non-empty.
+ *
+ * @param string $mask The character to display instead of the actual input characters.
+ * Default is '*'. Can be any single character or string.
+ *
+ * @param string|null $default An optional default value to use if the user provides
+ * empty input. If provided, it will be shown in the prompt.
+ *
+ * @param InputValidator|null $validator An optional validator to validate the input.
+ * If validation fails, the user will be prompted again.
+ *
+ * @return string|null Returns the actual input value (not masked) if valid input is provided,
+ * or null if the prompt is empty.
+ *
+ * @since 1.1.0
+ */
+ public function getMaskedInput(string $prompt, string $mask = '*', ?string $default = null, ?InputValidator $validator = null): ?string {
+ $trimmed = trim($prompt);
+
+ if (strlen($trimmed) > 0) {
+ do {
+ $this->prints($trimmed, [
+ 'color' => 'gray',
+ 'bold' => true
+ ]);
+
+ if ($default !== null) {
+ $this->prints(" Enter = '".$default."'", [
+ 'color' => 'light-blue'
+ ]);
+ }
+ $this->println();
+ $input = trim($this->readMaskedLine($mask));
+
+ $check = $this->getInputHelper($input, $validator, $default);
+
+ if ($check['valid']) {
+ return $check['value'];
+ }
+ } while (true);
+ }
+
+ return null;
+ } /**
* Returns the stream at which the command is sing to read inputs.
*
* @return null|InputStream If the stream is set, it will be returned as
@@ -964,7 +1014,63 @@ public function readInteger(string $prompt, ?int $default = null) : int {
public function readln() : string {
return $this->getInputStream()->readLine();
}
-
+ /**
+ * Reads a line from input stream with character masking.
+ *
+ * This method reads input character by character and displays mask characters
+ * instead of the actual input. It handles backspace for character deletion
+ * and ignores special keys like ESC and arrow keys.
+ *
+ * @param string $mask The character to display instead of actual input characters.
+ *
+ * @return string The actual input string (unmasked).
+ *
+ * @since 1.1.0
+ */
+ private function readMaskedLine(string $mask = '*'): string {
+ $input = '';
+
+ // For testing with ArrayInputStream, read the whole line at once
+ if ($this->getInputStream() instanceof \WebFiori\Cli\Streams\ArrayInputStream) {
+ $input = $this->getInputStream()->readLine();
+ // Simulate masking output for testing
+ $this->prints(str_repeat($mask, strlen($input)));
+ $this->println();
+ return $input;
+ }
+
+ // Set terminal to raw mode with echo disabled for real-time character reading
+ $sttyMode = null;
+ if (function_exists('shell_exec') && PHP_OS_FAMILY !== 'Windows') {
+ $sttyMode = shell_exec('stty -g 2>/dev/null');
+ shell_exec('stty -echo -icanon 2>/dev/null');
+ }
+
+ try {
+ // For real terminal input, read character by character
+ while (true) {
+ $char = KeysMap::readAndTranslate($this->getInputStream());
+
+ if ($char === 'LF' || $char === 'CR' || $char === '') {
+ break;
+ } elseif ($char === 'BACKSPACE' && strlen($input) > 0) {
+ $input = substr($input, 0, -1);
+ $this->prints("\x08 \x08"); // Backspace, space, backspace
+ } elseif ($char !== 'BACKSPACE' && $char !== 'ESC' && $char !== 'DOWN' && $char !== 'UP' && $char !== 'LEFT' && $char !== 'RIGHT') {
+ $input .= $char === 'SPACE' ? ' ' : $char;
+ $this->prints($mask);
+ }
+ }
+ } finally {
+ // Restore terminal settings
+ if ($sttyMode !== null) {
+ shell_exec('stty ' . $sttyMode . ' 2>/dev/null');
+ }
+ }
+
+ $this->println();
+ return $input;
+ }
/**
* Reads a string that represents class namespace.
*
diff --git a/WebFiori/Cli/Commands/MakeCommand.php b/WebFiori/Cli/Commands/MakeCommand.php
new file mode 100644
index 0000000..832f360
--- /dev/null
+++ b/WebFiori/Cli/Commands/MakeCommand.php
@@ -0,0 +1,221 @@
+templateManager = new TemplateManager();
+
+ parent::__construct('make:command', [
+ '--name' => [
+ ArgumentOption::DESCRIPTION => 'The name of the command (e.g., "user:create")',
+ ArgumentOption::OPTIONAL => false
+ ],
+ '--class' => [
+ ArgumentOption::DESCRIPTION => 'The class name (e.g., "CreateUserCommand")',
+ ArgumentOption::OPTIONAL => true
+ ],
+ '--path' => [
+ ArgumentOption::DESCRIPTION => 'Output directory path',
+ ArgumentOption::OPTIONAL => true,
+ ArgumentOption::DEFAULT => 'commands'
+ ],
+ '--namespace' => [
+ ArgumentOption::DESCRIPTION => 'PHP namespace for the command class',
+ ArgumentOption::OPTIONAL => true
+ ],
+ '--interactive' => [
+ ArgumentOption::DESCRIPTION => 'Generate command with interactive prompts',
+ ArgumentOption::OPTIONAL => true
+ ],
+ '--args' => [
+ ArgumentOption::DESCRIPTION => 'Add command arguments (comma-separated)',
+ ArgumentOption::OPTIONAL => true
+ ],
+ '--template' => [
+ ArgumentOption::DESCRIPTION => 'Template type to use',
+ ArgumentOption::OPTIONAL => true,
+ ArgumentOption::VALUES => $this->templateManager->getAvailableTemplates(),
+ ArgumentOption::DEFAULT => 'basic'
+ ]
+ ], 'Generate a new CLI command class with scaffolding');
+ }
+
+ public function exec(): int {
+ $this->println('🚀 WebFiori CLI Command Generator');
+ $this->println('=================================');
+ $this->println();
+
+ // Get command details
+ $commandName = $this->getArgValue('--name');
+ $className = $this->getArgValue('--class') ?? $this->generateClassName($commandName);
+ $outputPath = $this->getArgValue('--path') ?? 'commands';
+ $namespace = $this->getArgValue('--namespace');
+ $template = $this->getArgValue('--template') ?? 'basic';
+ $interactive = $this->isArgProvided('--interactive');
+ $args = $this->getArgValue('--args');
+
+ // Interactive mode for missing details
+ if (!$namespace) {
+ $namespace = $this->getInput('Enter namespace (optional): ') ?: null;
+ }
+
+ // Validate inputs
+ if (!$this->validateInputs($commandName, $className)) {
+ return 1;
+ }
+
+ // Generate command
+ try {
+ $filePath = $this->generateCommand([
+ 'name' => $commandName,
+ 'class' => $className,
+ 'path' => $outputPath,
+ 'namespace' => $namespace,
+ 'template' => $template,
+ 'interactive' => $interactive,
+ 'args' => $args ? explode(',', $args) : []
+ ]);
+
+ $this->success("✅ Command generated successfully!");
+ $this->info("📁 File: $filePath");
+ $this->info("🏷️ Class: $className");
+ $this->info("⚡ Command: $commandName");
+
+ $this->println();
+ $this->println("Next steps:");
+ $this->println("1. Register the command in your application");
+ $this->println("2. Implement the exec() method logic");
+ $this->println("3. Add any additional arguments or validation");
+
+ return 0;
+ } catch (\Exception $e) {
+ $this->error("❌ Failed to generate command: " . $e->getMessage());
+ return 1;
+ }
+ }
+
+ /**
+ * Generate class name from command name.
+ */
+ private function generateClassName(string $commandName): string {
+ // Convert command-name or namespace:command to ClassName
+ $parts = preg_split('/[:\-_]/', $commandName);
+ $className = '';
+
+ foreach ($parts as $part) {
+ $className .= ucfirst(strtolower($part));
+ }
+
+ return $className . 'Command';
+ }
+
+ /**
+ * Validate command inputs.
+ */
+ private function validateInputs(string $commandName, string $className): bool {
+ // Validate command name
+ if (!preg_match('/^[a-z][a-z0-9\-:_]*$/', $commandName)) {
+ $this->error('Command name must start with a letter and contain only lowercase letters, numbers, hyphens, colons, and underscores.');
+ return false;
+ }
+
+ // Validate class name
+ if (!preg_match('/^[A-Z][a-zA-Z0-9]*$/', $className)) {
+ $this->error('Class name must be a valid PHP class name (PascalCase).');
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Generate the command file.
+ */
+ private function generateCommand(array $config): string {
+ $content = $this->templateManager->processTemplate($config['template'], [
+ 'namespace' => $config['namespace'] ? "namespace {$config['namespace']};\n\n" : '',
+ 'use_statements' => $this->generateUseStatements($config),
+ 'class_name' => $config['class'],
+ 'command_name' => $config['name'],
+ 'command_description' => "Description for {$config['name']} command",
+ 'arguments' => $this->generateArguments($config['args'])
+ ]);
+
+ // Ensure output directory exists
+ $outputDir = $config['path'];
+ if (!is_dir($outputDir)) {
+ mkdir($outputDir, 0755, true);
+ }
+
+ // Generate file path
+ $fileName = $config['class'] . '.php';
+ $filePath = rtrim($outputDir, '/') . '/' . $fileName;
+
+ // Check if file exists
+ if (file_exists($filePath)) {
+ $overwrite = $this->confirm("File $filePath already exists. Overwrite?");
+ if (!$overwrite) {
+ throw new \Exception("File already exists and overwrite was declined.");
+ }
+ }
+
+ // Write file
+ file_put_contents($filePath, $content);
+
+ return $filePath;
+ }
+
+ /**
+ * Generate use statements.
+ */
+ private function generateUseStatements(array $config): string {
+ $uses = [
+ 'use WebFiori\Cli\Command;',
+ 'use WebFiori\Cli\ArgumentOption;'
+ ];
+
+ if ($config['interactive'] || $config['template'] === 'interactive') {
+ $uses[] = 'use WebFiori\Cli\InputValidator;';
+ }
+
+ return implode("\n", $uses);
+ }
+
+ /**
+ * Generate command arguments array.
+ */
+ private function generateArguments(array $args): string {
+ if (empty($args)) {
+ return '[]';
+ }
+
+ $argStrings = [];
+ foreach ($args as $arg) {
+ $arg = trim($arg);
+ $argName = '--' . strtolower(str_replace(' ', '-', $arg));
+ $argStrings[] = " '$argName' => [\n" .
+ " ArgumentOption::DESCRIPTION => 'Description for $arg',\n" .
+ " ArgumentOption::OPTIONAL => true\n" .
+ " ]";
+ }
+
+ return "[\n" . implode(",\n", $argStrings) . "\n ]";
+ }
+}
diff --git a/WebFiori/Cli/Table/TableData.php b/WebFiori/Cli/Table/TableData.php
index 631355f..c6d14b0 100644
--- a/WebFiori/Cli/Table/TableData.php
+++ b/WebFiori/Cli/Table/TableData.php
@@ -95,7 +95,7 @@ public static function fromCsv(string $csv, bool $hasHeaders = true, string $del
continue;
}
- $row = str_getcsv($line, $delimiter);
+ $row = str_getcsv($line, $delimiter, '"', '\\');
if ($hasHeaders && $headers === null) {
$headers = $row;
diff --git a/WebFiori/Cli/Templates/TemplateManager.php b/WebFiori/Cli/Templates/TemplateManager.php
new file mode 100644
index 0000000..aa757f3
--- /dev/null
+++ b/WebFiori/Cli/Templates/TemplateManager.php
@@ -0,0 +1,55 @@
+templatesPath = $templatesPath ?? __DIR__ . '/stubs';
+ }
+
+ /**
+ * Get template content by name.
+ */
+ public function getTemplate(string $name): string {
+ $templateFile = $this->templatesPath . '/' . $name . '.stub';
+
+ if (!file_exists($templateFile)) {
+ throw new \InvalidArgumentException("Template '$name' not found at: $templateFile");
+ }
+
+ return file_get_contents($templateFile);
+ }
+
+ /**
+ * Get available templates.
+ */
+ public function getAvailableTemplates(): array {
+ $templates = [];
+ $files = glob($this->templatesPath . '/*.stub');
+
+ foreach ($files as $file) {
+ $templates[] = basename($file, '.stub');
+ }
+
+ return $templates;
+ }
+
+ /**
+ * Process template with variables.
+ */
+ public function processTemplate(string $template, array $variables): string {
+ $content = $this->getTemplate($template);
+
+ foreach ($variables as $key => $value) {
+ $content = str_replace('{{' . $key . '}}', $value, $content);
+ }
+
+ return $content;
+ }
+}
diff --git a/WebFiori/Cli/Templates/stubs/basic.stub b/WebFiori/Cli/Templates/stubs/basic.stub
new file mode 100644
index 0000000..85e8b45
--- /dev/null
+++ b/WebFiori/Cli/Templates/stubs/basic.stub
@@ -0,0 +1,23 @@
+println('🚀 Executing {{command_name}} command...');
+
+ // TODO: Implement your command logic here
+
+ $this->success('✅ Command completed successfully!');
+ return 0;
+ }
+}
diff --git a/WebFiori/Cli/Templates/stubs/crud.stub b/WebFiori/Cli/Templates/stubs/crud.stub
new file mode 100644
index 0000000..52c8b94
--- /dev/null
+++ b/WebFiori/Cli/Templates/stubs/crud.stub
@@ -0,0 +1,73 @@
+ [
+ ArgumentOption::DESCRIPTION => 'CRUD action to perform',
+ ArgumentOption::OPTIONAL => true,
+ ArgumentOption::VALUES => ['create', 'read', 'show', 'update', 'delete', 'list'],
+ ArgumentOption::DEFAULT => 'list'
+ ]
+ ]), '{{command_description}}');
+ }
+
+ public function exec(): int {
+ $action = $this->getArgValue('--action') ?? 'list';
+
+ switch ($action) {
+ case 'create':
+ return $this->createRecord();
+ case 'read':
+ case 'show':
+ return $this->showRecord();
+ case 'update':
+ return $this->updateRecord();
+ case 'delete':
+ return $this->deleteRecord();
+ case 'list':
+ default:
+ return $this->listRecords();
+ }
+ }
+
+ private function createRecord(): int {
+ $this->info('Creating new record...');
+ // TODO: Implement create logic
+ $this->success('✅ Record created successfully!');
+ return 0;
+ }
+
+ private function showRecord(): int {
+ $this->info('Showing record...');
+ // TODO: Implement show logic
+ return 0;
+ }
+
+ private function updateRecord(): int {
+ $this->info('Updating record...');
+ // TODO: Implement update logic
+ $this->success('✅ Record updated successfully!');
+ return 0;
+ }
+
+ private function deleteRecord(): int {
+ $this->info('Deleting record...');
+ // TODO: Implement delete logic
+ $this->success('✅ Record deleted successfully!');
+ return 0;
+ }
+
+ private function listRecords(): int {
+ $this->info('Listing records...');
+ // TODO: Implement list logic
+ return 0;
+ }
+}
diff --git a/WebFiori/Cli/Templates/stubs/interactive.stub b/WebFiori/Cli/Templates/stubs/interactive.stub
new file mode 100644
index 0000000..22cb7b2
--- /dev/null
+++ b/WebFiori/Cli/Templates/stubs/interactive.stub
@@ -0,0 +1,43 @@
+println('🔧 Interactive Command Setup');
+ $this->println('==========================');
+
+ // Get user input
+ $name = $this->getInput('Enter name: ');
+ $email = $this->getInput('Enter email: ', null, new InputValidator(function($email) {
+ return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
+ }, 'Please enter a valid email address!'));
+
+ // Confirm action
+ if ($this->confirm('Proceed with the operation?')) {
+ $this->processData($name, $email);
+ $this->success('✅ Operation completed successfully!');
+ } else {
+ $this->info('Operation cancelled.');
+ }
+
+ return 0;
+ }
+
+ /**
+ * Process the collected data.
+ */
+ private function processData(string $name, string $email): void {
+ // TODO: Implement data processing logic
+ $this->info("Processing data for: $name ($email)");
+ }
+}
diff --git a/examples/01-basic-hello-world/README.md b/examples/01-basic-hello-world/README.md
index 1a03ffa..fed2549 100644
--- a/examples/01-basic-hello-world/README.md
+++ b/examples/01-basic-hello-world/README.md
@@ -128,3 +128,18 @@ class HelloCommand extends Command {
```
This example serves as the foundation for understanding WebFiori CLI basics before moving to more advanced features.
+
+## Related Examples
+
+### Next Steps
+- **[02-arguments-and-options](../02-arguments-and-options/)** - Learn advanced argument handling and validation
+- **[03-user-input](../03-user-input/)** - Add interactive user input to your commands
+- **[04-output-formatting](../04-output-formatting/)** - Enhance output with colors and formatting
+
+### Advanced Features
+- **[10-multi-command-app](../10-multi-command-app/)** - Build complete CLI applications
+- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands automatically
+
+### Similar Concepts
+- **[05-interactive-commands](../05-interactive-commands/)** - Interactive command workflows
+- **[11-masked-input](../11-masked-input/)** - Secure input handling
diff --git a/examples/02-arguments-and-options/README.md b/examples/02-arguments-and-options/README.md
index 0c3b8a9..c7358a1 100644
--- a/examples/02-arguments-and-options/README.md
+++ b/examples/02-arguments-and-options/README.md
@@ -350,3 +350,22 @@ class UserProfileCommand extends Command {
```
This example demonstrates advanced CLI application development with proper validation, error handling, and user experience design.
+
+## Related Examples
+
+### Prerequisites
+- **[01-basic-hello-world](../01-basic-hello-world/)** - Start here for basic command concepts
+
+### Next Steps
+- **[03-user-input](../03-user-input/)** - Interactive input and validation
+- **[11-masked-input](../11-masked-input/)** - Secure input for sensitive data
+- **[04-output-formatting](../04-output-formatting/)** - Enhanced output styling
+
+### Advanced Applications
+- **[10-multi-command-app](../10-multi-command-app/)** - Complete CLI applications
+- **[09-database-ops](../09-database-ops/)** - Database operations with validation
+- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with arguments
+
+### Similar Concepts
+- **[05-interactive-commands](../05-interactive-commands/)** - Menu-driven interfaces
+- **[08-file-processing](../08-file-processing/)** - File operations with validation
diff --git a/examples/03-user-input/README.md b/examples/03-user-input/README.md
index 72a9cad..b30caad 100644
--- a/examples/03-user-input/README.md
+++ b/examples/03-user-input/README.md
@@ -373,3 +373,25 @@ private function collectBasicInfo() {
- **User Experience**: Rich formatting with emojis and clear section divisions
This example demonstrates advanced user input handling suitable for complex CLI applications requiring data collection and validation.
+
+## Related Examples
+
+### Prerequisites
+- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure
+- **[02-arguments-and-options](../02-arguments-and-options/)** - Argument handling and validation
+
+### Enhanced Input Methods
+- **[11-masked-input](../11-masked-input/)** - Secure input for passwords and sensitive data
+- **[05-interactive-commands](../05-interactive-commands/)** - Menu-driven interactive workflows
+
+### Output Enhancement
+- **[04-output-formatting](../04-output-formatting/)** - Colors, styles, and formatting
+- **[06-table-display](../06-table-display/)** - Structured data presentation
+- **[07-progress-bars](../07-progress-bars/)** - Visual progress indicators
+
+### Complete Applications
+- **[10-multi-command-app](../10-multi-command-app/)** - Full CLI applications with user management
+- **[09-database-ops](../09-database-ops/)** - Database operations with user input
+
+### Development Tools
+- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate interactive commands automatically
diff --git a/examples/04-output-formatting/README.md b/examples/04-output-formatting/README.md
index b57f0c7..a9038e1 100644
--- a/examples/04-output-formatting/README.md
+++ b/examples/04-output-formatting/README.md
@@ -562,3 +562,25 @@ private function showSpinnerAnimation(): void {
```
This example demonstrates professional CLI output formatting suitable for creating visually appealing and user-friendly command-line applications.
+
+## Related Examples
+
+### Prerequisites
+- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command and output concepts
+
+### Enhanced Output Features
+- **[06-table-display](../06-table-display/)** - Structured data in formatted tables
+- **[07-progress-bars](../07-progress-bars/)** - Professional progress indicators
+- **[05-interactive-commands](../05-interactive-commands/)** - Interactive menus with formatting
+
+### Input with Formatting
+- **[03-user-input](../03-user-input/)** - User input with formatted prompts
+- **[11-masked-input](../11-masked-input/)** - Secure input with visual feedback
+
+### Complete Applications
+- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with consistent formatting
+- **[09-database-ops](../09-database-ops/)** - Database operations with formatted output
+- **[08-file-processing](../08-file-processing/)** - File operations with status formatting
+
+### Development Tools
+- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with formatting templates
diff --git a/examples/05-interactive-commands/README.md b/examples/05-interactive-commands/README.md
index a41aef9..bce5db7 100644
--- a/examples/05-interactive-commands/README.md
+++ b/examples/05-interactive-commands/README.md
@@ -316,3 +316,26 @@ private function showContextHelp(): void {
- **[03-user-input](../03-user-input/)**: Input validation and handling
- **[04-output-formatting](../04-output-formatting/)**: ANSI colors and formatting
+## Related Examples
+
+### Prerequisites
+- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure
+- **[03-user-input](../03-user-input/)** - User input and validation fundamentals
+- **[04-output-formatting](../04-output-formatting/)** - ANSI colors and formatting
+
+### Enhanced Interactive Features
+- **[11-masked-input](../11-masked-input/)** - Secure input for sensitive operations
+- **[02-arguments-and-options](../02-arguments-and-options/)** - Command arguments and options
+
+### Visual Enhancements
+- **[06-table-display](../06-table-display/)** - Display data in formatted tables
+- **[07-progress-bars](../07-progress-bars/)** - Visual progress indicators
+
+### Complete Applications
+- **[10-multi-command-app](../10-multi-command-app/)** - Full CLI applications with menus
+- **[09-database-ops](../09-database-ops/)** - Database management with interactive menus
+- **[08-file-processing](../08-file-processing/)** - File operations with user interaction
+
+### Development Tools
+- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate interactive commands automatically
+
diff --git a/examples/06-table-display/README.md b/examples/06-table-display/README.md
index 14c6b63..b7d2cce 100644
--- a/examples/06-table-display/README.md
+++ b/examples/06-table-display/README.md
@@ -298,3 +298,25 @@ php main.php table-demo --help
- **[05-interactive-commands](../05-interactive-commands/)** - Interactive menu systems
- **[08-file-processing](../08-file-processing/)** - File data processing
- **[10-multi-command-app](../10-multi-command-app/)** - Complete CLI applications
+
+## Related Examples
+
+### Prerequisites
+- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure
+- **[04-output-formatting](../04-output-formatting/)** - Colors and formatting basics
+
+### Enhanced Display Features
+- **[07-progress-bars](../07-progress-bars/)** - Visual progress indicators
+- **[05-interactive-commands](../05-interactive-commands/)** - Interactive menus with tables
+
+### Data Sources
+- **[08-file-processing](../08-file-processing/)** - Process files and display results in tables
+- **[09-database-ops](../09-database-ops/)** - Database queries with table output
+- **[03-user-input](../03-user-input/)** - Collect data and display in tables
+
+### Complete Applications
+- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with data display
+- **[02-arguments-and-options](../02-arguments-and-options/)** - Commands with formatted output
+
+### Development Tools
+- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with table display
diff --git a/examples/07-progress-bars/README.md b/examples/07-progress-bars/README.md
index 72769e2..1b3b6a4 100644
--- a/examples/07-progress-bars/README.md
+++ b/examples/07-progress-bars/README.md
@@ -302,3 +302,24 @@ php main.php progress-demo --style=custom --items=20 --delay=50
- **[06-table-display](../06-table-display/)** - Data presentation techniques
- **[08-file-processing](../08-file-processing/)** - File operations with progress
- **[10-multi-command-app](../10-multi-command-app/)** - Complete CLI applications
+## Related Examples
+
+### Prerequisites
+- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure
+- **[04-output-formatting](../04-output-formatting/)** - Colors and formatting
+
+### Visual Enhancement
+- **[06-table-display](../06-table-display/)** - Structured data display
+- **[05-interactive-commands](../05-interactive-commands/)** - Interactive workflows with progress
+
+### Long-Running Operations
+- **[08-file-processing](../08-file-processing/)** - File operations with progress tracking
+- **[09-database-ops](../09-database-ops/)** - Database operations with progress indicators
+- **[03-user-input](../03-user-input/)** - Multi-step processes with progress
+
+### Complete Applications
+- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with progress feedback
+- **[02-arguments-and-options](../02-arguments-and-options/)** - Commands with progress reporting
+
+### Development Tools
+- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with progress bars
diff --git a/examples/08-file-processing/README.md b/examples/08-file-processing/README.md
index c530980..d3b0f60 100644
--- a/examples/08-file-processing/README.md
+++ b/examples/08-file-processing/README.md
@@ -274,3 +274,25 @@ class FileProcessCommand extends Command {
- **[04-output-formatting](../04-output-formatting/)** - Text formatting and colors
- **[07-progress-bars](../07-progress-bars/)** - Progress tracking for file operations
- **[10-multi-command-app](../10-multi-command-app/)** - Complete CLI applications
+## Related Examples
+
+### Prerequisites
+- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure
+- **[02-arguments-and-options](../02-arguments-and-options/)** - File path arguments and validation
+
+### Enhanced Processing
+- **[07-progress-bars](../07-progress-bars/)** - Visual progress for file operations
+- **[06-table-display](../06-table-display/)** - Display file data in formatted tables
+- **[04-output-formatting](../04-output-formatting/)** - Formatted status messages
+
+### User Interaction
+- **[03-user-input](../03-user-input/)** - Interactive file selection
+- **[05-interactive-commands](../05-interactive-commands/)** - File operation menus
+- **[11-masked-input](../11-masked-input/)** - Secure file path input
+
+### Complete Applications
+- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with file management
+- **[09-database-ops](../09-database-ops/)** - Database import/export from files
+
+### Development Tools
+- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate file processing commands
diff --git a/examples/09-database-ops/README.md b/examples/09-database-ops/README.md
index 6788a8c..a33f8b5 100644
--- a/examples/09-database-ops/README.md
+++ b/examples/09-database-ops/README.md
@@ -599,3 +599,25 @@ class QueryOptimizer {
}
}
```
+## Related Examples
+
+### Prerequisites
+- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure
+- **[02-arguments-and-options](../02-arguments-and-options/)** - Database connection arguments
+
+### Enhanced Features
+- **[06-table-display](../06-table-display/)** - Display query results in tables
+- **[07-progress-bars](../07-progress-bars/)** - Progress for long database operations
+- **[05-interactive-commands](../05-interactive-commands/)** - Database management menus
+
+### User Interaction
+- **[03-user-input](../03-user-input/)** - Interactive database configuration
+- **[11-masked-input](../11-masked-input/)** - Secure database password input
+- **[04-output-formatting](../04-output-formatting/)** - Formatted database status
+
+### Data Processing
+- **[08-file-processing](../08-file-processing/)** - Import/export database data
+- **[10-multi-command-app](../10-multi-command-app/)** - Complete database CLI applications
+
+### Development Tools
+- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate database operation commands
diff --git a/examples/10-multi-command-app/README.md b/examples/10-multi-command-app/README.md
index 7b1facd..afb40ad 100644
--- a/examples/10-multi-command-app/README.md
+++ b/examples/10-multi-command-app/README.md
@@ -713,3 +713,23 @@ class ApiClient {
}
}
```
+## Related Examples
+
+### Building Blocks (Start Here)
+- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure
+- **[02-arguments-and-options](../02-arguments-and-options/)** - Command arguments and validation
+- **[03-user-input](../03-user-input/)** - User input and interaction
+
+### Feature Integration
+- **[04-output-formatting](../04-output-formatting/)** - Professional output styling
+- **[05-interactive-commands](../05-interactive-commands/)** - Menu-driven interfaces
+- **[06-table-display](../06-table-display/)** - Data presentation in tables
+- **[07-progress-bars](../07-progress-bars/)** - Visual progress feedback
+- **[11-masked-input](../11-masked-input/)** - Secure input handling
+
+### Specialized Operations
+- **[08-file-processing](../08-file-processing/)** - File management commands
+- **[09-database-ops](../09-database-ops/)** - Database operation commands
+
+### Development Tools
+- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands for your application
diff --git a/examples/11-masked-input/README.md b/examples/11-masked-input/README.md
new file mode 100644
index 0000000..92bf0cb
--- /dev/null
+++ b/examples/11-masked-input/README.md
@@ -0,0 +1,187 @@
+# Masked Input Example
+
+This example demonstrates the **masked input functionality** in WebFiori CLI, which allows secure entry of sensitive data like passwords, PINs, and tokens.
+
+## Features Demonstrated
+
+- **Basic Password Input**: Default asterisk (*) masking with validation
+- **Custom Mask Characters**: Use different characters (•, #, X, -) for masking
+- **Input Validation**: Enforce security requirements and format validation
+- **Default Values**: Optional default values for sensitive fields
+- **Confirmation Prompts**: Verify critical inputs by asking twice
+
+## Running the Example
+
+### Basic Usage
+```bash
+php main.php secure-input
+```
+
+### Run Specific Demos
+```bash
+# Password demo only
+php main.php secure-input --demo=password
+
+# PIN demo with custom mask
+php main.php secure-input --demo=pin
+
+# Token demo with default value
+php main.php secure-input --demo=token
+
+# All demos (default)
+php main.php secure-input --demo=all
+```
+
+## Code Examples
+
+### Basic Masked Input
+```php
+// Simple password input with default * masking
+$password = $this->getMaskedInput('Enter password: ');
+```
+
+### Custom Mask Character
+```php
+// Use # characters for PIN masking
+$pin = $this->getMaskedInput('Enter PIN: ', null, null, '#');
+```
+
+### With Validation
+```php
+$validator = new InputValidator(function($password) {
+ return strlen($password) >= 8 &&
+ preg_match('/[A-Z]/', $password) &&
+ preg_match('/[0-9]/', $password);
+}, 'Password must be 8+ chars with uppercase and number!');
+
+$password = $this->getMaskedInput('Password: ', null, $validator);
+```
+
+### With Default Value
+```php
+// Provide a default token value
+$token = $this->getMaskedInput('API Token: ', 'default-token', null, '•');
+```
+
+## Method Signature
+
+```php
+public function getMaskedInput(
+ string $prompt, // The prompt to display
+ ?string $default = null, // Optional default value
+ ?InputValidator $validator = null, // Optional input validator
+ string $mask = '*' // Mask character (default: *)
+): ?string
+```
+
+## Security Features
+
+### Input Masking
+- Characters are masked as you type
+- Only mask characters are displayed in terminal
+- Actual input is captured securely
+- Supports backspace for corrections
+
+### Validation Support
+- Enforce minimum length requirements
+- Validate character patterns (uppercase, numbers, symbols)
+- Custom validation logic
+- Automatic retry on validation failure
+
+### Safe Handling
+- Input is trimmed automatically
+- Empty prompts return null safely
+- Works with existing stream abstraction
+- Compatible with testing framework
+
+## Use Cases
+
+### 1. User Authentication
+```php
+$password = $this->getMaskedInput('Login Password: ');
+$confirmPassword = $this->getMaskedInput('Confirm Password: ');
+
+if ($password !== $confirmPassword) {
+ $this->error('Passwords do not match!');
+ return 1;
+}
+```
+
+### 2. API Configuration
+```php
+$apiKey = $this->getMaskedInput('API Key: ', null, null, '•');
+$secret = $this->getMaskedInput('API Secret: ', null, null, '-');
+```
+
+### 3. Database Setup
+```php
+$dbPassword = $this->getMaskedInput('Database Password: ');
+
+$validator = new InputValidator(function($host) {
+ return filter_var($host, FILTER_VALIDATE_IP) ||
+ filter_var($host, FILTER_VALIDATE_DOMAIN);
+}, 'Invalid host format!');
+
+$dbHost = $this->getInput('Database Host: ', 'localhost', $validator);
+```
+
+### 4. Secure Token Entry
+```php
+$jwtSecret = $this->getMaskedInput('JWT Secret: ', null,
+ new InputValidator(function($secret) {
+ return strlen($secret) >= 32;
+ }, 'JWT secret must be at least 32 characters!')
+);
+```
+
+## Interactive Demo Features
+
+The example includes several interactive demonstrations:
+
+1. **Password Demo**: Shows validation with security requirements
+2. **PIN Demo**: Demonstrates custom mask characters (#)
+3. **Token Demo**: Shows default values with bullet (•) masking
+4. **Advanced Demo**: Multiple scenarios including confirmation prompts
+
+## Testing
+
+The masked input functionality is fully testable using the existing `CommandTestCase` framework:
+
+```php
+$output = $this->executeSingleCommand($command, [], ['secret123']);
+$this->assertContains('Password received: secret123', $output);
+```
+
+## Best Practices
+
+1. **Always validate sensitive input** for security requirements
+2. **Use appropriate mask characters** for different data types
+3. **Implement confirmation prompts** for critical operations
+4. **Never log or display** the actual sensitive values
+5. **Provide clear error messages** for validation failures
+
+---
+
+**Ready to secure your CLI applications?** Try the different demo modes to see masked input in action!
+## Related Examples
+
+### Prerequisites
+- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure
+- **[03-user-input](../03-user-input/)** - User input fundamentals
+
+### Enhanced Input Methods
+- **[02-arguments-and-options](../02-arguments-and-options/)** - Command arguments with validation
+- **[05-interactive-commands](../05-interactive-commands/)** - Interactive menus and workflows
+
+### Visual Enhancement
+- **[04-output-formatting](../04-output-formatting/)** - Colors and formatting for prompts
+- **[06-table-display](../06-table-display/)** - Display collected data in tables
+- **[07-progress-bars](../07-progress-bars/)** - Progress indicators for data processing
+
+### Complete Applications
+- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with secure authentication
+- **[09-database-ops](../09-database-ops/)** - Database operations with secure credentials
+- **[08-file-processing](../08-file-processing/)** - File operations with secure paths
+
+### Development Tools
+- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with masked input
diff --git a/examples/11-masked-input/SecureInputCommand.php b/examples/11-masked-input/SecureInputCommand.php
new file mode 100644
index 0000000..b6b6fdb
--- /dev/null
+++ b/examples/11-masked-input/SecureInputCommand.php
@@ -0,0 +1,143 @@
+ [
+ ArgumentOption::DESCRIPTION => 'Type of demo to run',
+ ArgumentOption::OPTIONAL => true,
+ ArgumentOption::VALUES => ['password', 'pin', 'token', 'all'],
+ ArgumentOption::DEFAULT => 'all'
+ ]
+ ], 'Demonstrates secure masked input functionality');
+ }
+
+ public function exec(): int {
+ $demo = $this->getArgValue('--demo') ?? 'all';
+
+ $this->println('🔒 WebFiori CLI - Masked Input Demo');
+ $this->println('===================================');
+ $this->println();
+
+ switch ($demo) {
+ case 'password':
+ $this->passwordDemo();
+ break;
+ case 'pin':
+ $this->pinDemo();
+ break;
+ case 'token':
+ $this->tokenDemo();
+ break;
+ case 'all':
+ default:
+ $this->passwordDemo();
+ $this->println();
+ $this->pinDemo();
+ $this->println();
+ $this->tokenDemo();
+ $this->println();
+ $this->advancedDemo();
+ break;
+ }
+
+ $this->println();
+ $this->success('✅ Demo completed successfully!');
+
+ return 0;
+ }
+
+ /**
+ * Demonstrates basic password input with validation.
+ */
+ private function passwordDemo(): void {
+ $this->info('📝 Password Demo - Basic masked input with validation');
+ $this->println('Enter a password (minimum 8 characters):');
+
+ $validator = new InputValidator(function($password) {
+ if (strlen($password) < 8) {
+ return false;
+ }
+ if (!preg_match('/[A-Z]/', $password)) {
+ return false;
+ }
+ if (!preg_match('/[0-9]/', $password)) {
+ return false;
+ }
+ return true;
+ }, 'Password must be at least 8 characters with uppercase letter and number!');
+
+ $password = $this->getMaskedInput('Password: ', '*', null, $validator);
+
+ $this->success("✅ Password accepted! Length: " . strlen($password));
+ $this->println(" Captured value: $password");
+ }
+
+ /**
+ * Demonstrates PIN input with custom mask character.
+ */
+ private function pinDemo(): void {
+ $this->info('🔢 PIN Demo - Custom mask character');
+ $this->println('Enter a 4-digit PIN (will be masked with # characters):');
+
+ $validator = new InputValidator(function($pin) {
+ return strlen($pin) === 4 && ctype_digit($pin);
+ }, 'PIN must be exactly 4 digits!');
+
+ $pin = $this->getMaskedInput('PIN: ', '#', null, $validator);
+
+ $this->success("✅ PIN accepted!");
+ $this->println(" Captured value: $pin");
+ }
+
+ /**
+ * Demonstrates token input with default value.
+ */
+ private function tokenDemo(): void {
+ $this->info('🎫 Token Demo - With default value');
+ $this->println('Enter API token (or press Enter for demo token):');
+
+ $token = $this->getMaskedInput('API Token: ', '•', 'demo-token-12345');
+
+ $this->success("✅ Token set!");
+ $this->println(" Captured value: $token");
+ }
+
+ /**
+ * Demonstrates advanced scenarios.
+ */
+ private function advancedDemo(): void {
+ $this->info('🚀 Advanced Demo - Multiple scenarios');
+
+ // Database password with confirmation
+ $this->println('Setting up database connection:');
+
+ $dbPassword = $this->getMaskedInput('Database Password: ');
+ $confirmPassword = $this->getMaskedInput('Confirm Password: ');
+
+ if ($dbPassword !== $confirmPassword) {
+ $this->error('❌ Passwords do not match!');
+ $this->println(" First: $dbPassword");
+ $this->println(" Second: $confirmPassword");
+ return;
+ }
+
+ $this->success('✅ Database password confirmed');
+ $this->println(" Captured value: $dbPassword");
+ }
+}
diff --git a/examples/11-masked-input/main.php b/examples/11-masked-input/main.php
new file mode 100644
index 0000000..969d23c
--- /dev/null
+++ b/examples/11-masked-input/main.php
@@ -0,0 +1,22 @@
+register(new SecureInputCommand());
+
+// Start the application
+exit($runner->start());
diff --git a/examples/12-command-scaffolding/README.md b/examples/12-command-scaffolding/README.md
new file mode 100644
index 0000000..09d94ca
--- /dev/null
+++ b/examples/12-command-scaffolding/README.md
@@ -0,0 +1,302 @@
+# Command Scaffolding Tools
+
+This example demonstrates the **command scaffolding functionality** in WebFiori CLI, which allows developers to quickly generate new command classes with proper structure, documentation, and templates.
+
+## Features
+
+- **Multiple Templates**: Basic, Interactive, CRUD, and File Processor templates
+- **Smart Naming**: Automatic class name generation from command names
+- **Namespace Support**: Generate commands with custom namespaces
+- **Argument Generation**: Automatically create command arguments
+- **Validation**: Input validation for command and class names
+- **Overwrite Protection**: Confirmation prompts for existing files
+
+## Running the Example
+
+### Basic Command Generation
+```bash
+# Generate a basic command
+php main.php make:command --name=hello-world
+
+# Generate with custom class name
+php main.php make:command --name=user:create --class=CreateUserCommand
+
+# Generate with namespace
+php main.php make:command --name=process-data --namespace="App\\Commands"
+```
+
+### Template-Based Generation
+```bash
+# Interactive command template
+php main.php make:command --name=setup-wizard --template=interactive
+
+# CRUD operations template
+php main.php make:command --name=user-manager --template=crud
+
+# File processor template
+php main.php make:command --name=file-converter --template=file-processor
+```
+
+### Advanced Options
+```bash
+# Generate with arguments
+php main.php make:command --name=backup-db --args="database,output-path,compress"
+
+# Custom output directory
+php main.php make:command --name=deploy --path=src/Commands
+
+# All options combined
+php main.php make:command \
+ --name=api:sync \
+ --class=ApiSyncCommand \
+ --namespace="MyApp\\Commands" \
+ --template=interactive \
+ --args="endpoint,token,timeout" \
+ --path=app/Commands
+```
+
+## Available Templates
+
+### 1. Basic Template
+Simple command structure with minimal boilerplate:
+```php
+class HelloWorldCommand extends Command {
+ public function __construct() {
+ parent::__construct('hello-world', [], 'Description');
+ }
+
+ public function exec(): int {
+ $this->println('🚀 Executing hello-world command...');
+ // TODO: Implement your command logic here
+ $this->success('✅ Command completed successfully!');
+ return 0;
+ }
+}
+```
+
+### 2. Interactive Template
+Command with user input and validation:
+```php
+public function exec(): int {
+ $name = $this->getInput('Enter name: ');
+ $email = $this->getInput('Enter email: ', null, new InputValidator(function($email) {
+ return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
+ }, 'Please enter a valid email address!'));
+
+ if ($this->confirm('Proceed with the operation?')) {
+ $this->processData($name, $email);
+ $this->success('✅ Operation completed successfully!');
+ }
+
+ return 0;
+}
+```
+
+### 3. CRUD Template
+Full CRUD operations structure:
+```php
+public function exec(): int {
+ $action = $this->getArgValue('--action') ?? 'list';
+
+ switch ($action) {
+ case 'create': return $this->createRecord();
+ case 'read': return $this->showRecord();
+ case 'update': return $this->updateRecord();
+ case 'delete': return $this->deleteRecord();
+ case 'list':
+ default: return $this->listRecords();
+ }
+}
+```
+
+### 4. File Processor Template
+File processing with error handling:
+```php
+public function exec(): int {
+ $inputFile = $this->getArgValue('--input');
+
+ if (!$inputFile || !file_exists($inputFile)) {
+ $this->error('Input file is required and must exist!');
+ return 1;
+ }
+
+ try {
+ $this->processFile($inputFile, $this->getArgValue('--output'));
+ $this->success('✅ File processed successfully!');
+ return 0;
+ } catch (\Exception $e) {
+ $this->error('❌ Error: ' . $e->getMessage());
+ return 1;
+ }
+}
+```
+
+## Generated Command Structure
+
+All generated commands include:
+
+### Proper Documentation
+```php
+/**
+ * CommandName - Generated CLI command.
+ *
+ * Description for command-name command
+ */
+class CommandNameCommand extends Command {
+```
+
+### Constructor with Arguments
+```php
+public function __construct() {
+ parent::__construct('command-name', [
+ '--input-file' => [
+ ArgumentOption::DESCRIPTION => 'Description for input file',
+ ArgumentOption::OPTIONAL => true
+ ]
+ ], 'Command description');
+}
+```
+
+### Structured Exec Method
+```php
+public function exec(): int {
+ // Template-specific implementation
+ return 0;
+}
+```
+
+### Additional Helper Methods
+Template-specific helper methods for common operations.
+
+## Smart Naming Conventions
+
+The scaffolding tool automatically converts command names to proper class names:
+
+| Command Name | Generated Class Name |
+|--------------|---------------------|
+| `hello-world` | `HelloWorldCommand` |
+| `user:create` | `UserCreateCommand` |
+| `api_sync` | `ApiSyncCommand` |
+| `process-data-file` | `ProcessDataFileCommand` |
+
+## Validation Features
+
+### Command Name Validation
+- Must start with a letter
+- Can contain lowercase letters, numbers, hyphens, colons, underscores
+- Examples: `hello`, `user:create`, `process-data`
+
+### Class Name Validation
+- Must be valid PHP class name (PascalCase)
+- Automatically generated if not provided
+- Examples: `HelloCommand`, `UserCreateCommand`
+
+### File Overwrite Protection
+```bash
+$ php main.php make:command --name=existing-command
+File /path/to/ExistingCommand.php already exists. Overwrite? (y/n): n
+❌ Failed to generate command: File already exists and overwrite was declined.
+```
+
+## Integration with Your Application
+
+After generating commands, integrate them into your application:
+
+### 1. Register the Command
+```php
+use App\Commands\GeneratedCommand;
+
+$runner = new Runner();
+$runner->register(new GeneratedCommand());
+```
+
+### 2. Implement Logic
+Edit the generated `exec()` method to add your specific functionality.
+
+### 3. Add Tests
+Create unit tests for your generated commands using `CommandTestCase`.
+
+## Best Practices
+
+### Naming Conventions
+- Use kebab-case for command names: `user-create`, `data-export`
+- Use namespaces for organization: `user:create`, `db:migrate`
+- Keep names descriptive but concise
+
+### Template Selection
+- **Basic**: Simple commands with minimal logic
+- **Interactive**: Commands requiring user input
+- **CRUD**: Data management commands
+- **File Processor**: File manipulation commands
+
+### Organization
+```
+app/
+├── Commands/
+│ ├── User/
+│ │ ├── CreateUserCommand.php
+│ │ └── DeleteUserCommand.php
+│ ├── Database/
+│ │ ├── MigrateCommand.php
+│ │ └── SeedCommand.php
+│ └── File/
+│ ├── ProcessCommand.php
+│ └── ConvertCommand.php
+```
+
+## Example Workflow
+
+### 1. Generate User Management Commands
+```bash
+# Create user command
+php main.php make:command --name=user:create --template=interactive --namespace="App\\Commands\\User"
+
+# List users command
+php main.php make:command --name=user:list --template=crud --namespace="App\\Commands\\User"
+
+# Delete user command
+php main.php make:command --name=user:delete --namespace="App\\Commands\\User"
+```
+
+### 2. Generate File Processing Commands
+```bash
+# CSV processor
+php main.php make:command --name=csv:process --template=file-processor --args="input,output,delimiter"
+
+# Image converter
+php main.php make:command --name=image:convert --template=file-processor --args="input,format,quality"
+```
+
+### 3. Generate API Commands
+```bash
+# API sync command
+php main.php make:command --name=api:sync --template=interactive --args="endpoint,token"
+
+# Data export command
+php main.php make:command --name=data:export --args="format,output,filter"
+```
+
+---
+
+**Ready to boost your development speed?** Use the scaffolding tools to generate well-structured commands in seconds!
+## Related Examples
+
+### Generated Command Examples
+- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic template structure
+- **[02-arguments-and-options](../02-arguments-and-options/)** - Commands with arguments (CRUD template)
+- **[03-user-input](../03-user-input/)** - Interactive commands (Interactive template)
+- **[08-file-processing](../08-file-processing/)** - File operations (File Processor template)
+
+### Enhanced Features for Generated Commands
+- **[04-output-formatting](../04-output-formatting/)** - Add formatting to generated commands
+- **[05-interactive-commands](../05-interactive-commands/)** - Interactive workflows
+- **[06-table-display](../06-table-display/)** - Data display in generated commands
+- **[07-progress-bars](../07-progress-bars/)** - Progress indicators
+- **[11-masked-input](../11-masked-input/)** - Secure input in generated commands
+
+### Complete Applications
+- **[10-multi-command-app](../10-multi-command-app/)** - Applications built with scaffolded commands
+- **[09-database-ops](../09-database-ops/)** - Database commands (perfect for CRUD template)
+
+### Development Workflow
+Use this scaffolding tool to quickly generate commands for any of the above examples!
diff --git a/examples/12-command-scaffolding/main.php b/examples/12-command-scaffolding/main.php
new file mode 100644
index 0000000..f568443
--- /dev/null
+++ b/examples/12-command-scaffolding/main.php
@@ -0,0 +1,23 @@
+register(new MakeCommand());
+
+// Start the application
+exit($runner->start());
diff --git a/examples/README.md b/examples/README.md
index f71ef77..62526f6 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -17,7 +17,7 @@ Building more sophisticated CLI applications.
- **[05-interactive-commands](05-interactive-commands/)** - Creating interactive command experiences
- **[07-progress-bars](07-progress-bars/)** - Visual progress indicators for long operations
-
+- **[11-masked-input](11-masked-input/)** - Secure input with character masking
### 🔴 **Advanced Examples**
Complex scenarios and advanced features.
@@ -89,7 +89,7 @@ Explore examples 10-13 for real-world applications:
|---------|----------|-------------|
| **Command Creation** | 01, 02, 10 | Basic to advanced command structures |
| **Arguments & Options** | 02, 13 | Parameter handling and validation |
-| **User Input** | 03, 05 | Interactive input and validation |
+| **User Input** | 03, 05, 11 | Interactive input, validation, and secure entry |
| **Output Formatting** | 04, 07 | Colors, styles, and progress bars |
| **Interactive Workflows** | 05, 10 | Menu systems and wizards |
| **Progress Indicators** | 07, 10, 13 | Visual feedback for operations |
diff --git a/tests/WebFiori/Tests/Cli/MakeCommandTest.php b/tests/WebFiori/Tests/Cli/MakeCommandTest.php
new file mode 100644
index 0000000..7ce555b
--- /dev/null
+++ b/tests/WebFiori/Tests/Cli/MakeCommandTest.php
@@ -0,0 +1,236 @@
+testOutputDir)) {
+ $this->removeDirectory($this->testOutputDir);
+ }
+ }
+
+ protected function tearDown(): void {
+ parent::tearDown();
+ // Clean up test directory
+ if (is_dir($this->testOutputDir)) {
+ $this->removeDirectory($this->testOutputDir);
+ }
+ }
+
+ /**
+ * Test basic command generation.
+ *
+ * @test
+ */
+ public function testBasicCommandGeneration() {
+ $command = new MakeCommand();
+
+ $output = $this->executeSingleCommand($command, [
+ '--name' => 'test-command',
+ '--class' => 'TestCommand',
+ '--path' => $this->testOutputDir
+ ], ['', 'y']); // Empty namespace, then confirm overwrite if needed
+
+ // Check for success message (flexible matching)
+ $found = false;
+ foreach ($output as $line) {
+ if (strpos($line, 'Command generated successfully!') !== false) {
+ $found = true;
+ break;
+ }
+ }
+ $this->assertTrue($found, 'Success message not found in output');
+ $this->assertEquals(0, $this->getExitCode());
+
+ // Check if file was created
+ $expectedFile = $this->testOutputDir . '/TestCommand.php';
+ $this->assertTrue(file_exists($expectedFile), "Command file should be created");
+
+ // Check file content
+ $content = file_get_contents($expectedFile);
+ $this->assertStringContainsString('class TestCommand extends Command', $content);
+ $this->assertStringContainsString("'test-command'", $content);
+ }
+
+ /**
+ * Test command generation with namespace.
+ *
+ * @test
+ */
+ public function testCommandGenerationWithNamespace() {
+ $command = new MakeCommand();
+
+ $output = $this->executeSingleCommand($command, [
+ '--name' => 'user:create',
+ '--namespace' => 'App\\Commands',
+ '--path' => $this->testOutputDir
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+
+ $expectedFile = $this->testOutputDir . '/UserCreateCommand.php';
+ $this->assertTrue(file_exists($expectedFile));
+
+ $content = file_get_contents($expectedFile);
+ $this->assertStringContainsString('namespace App\\Commands;', $content);
+ $this->assertStringContainsString('class UserCreateCommand extends Command', $content);
+ }
+
+ /**
+ * Test interactive template generation.
+ *
+ * @test
+ */
+ public function testInteractiveTemplate() {
+ $command = new MakeCommand();
+
+ $output = $this->executeSingleCommand($command, [
+ '--name' => 'setup-wizard',
+ '--template' => 'interactive',
+ '--path' => $this->testOutputDir
+ ], ['']); // Empty namespace input
+
+ $this->assertEquals(0, $this->getExitCode());
+
+ $expectedFile = $this->testOutputDir . '/SetupWizardCommand.php';
+ $content = file_get_contents($expectedFile);
+
+ $this->assertStringContainsString('InputValidator', $content);
+ $this->assertStringContainsString('getInput(', $content);
+ $this->assertStringContainsString('confirm(', $content);
+ }
+
+ /**
+ * Test CRUD template generation.
+ *
+ * @test
+ */
+ public function testCrudTemplate() {
+ $command = new MakeCommand();
+
+ $output = $this->executeSingleCommand($command, [
+ '--name' => 'user-manager',
+ '--template' => 'crud',
+ '--path' => $this->testOutputDir
+ ], ['']); // Empty namespace input
+
+ $this->assertEquals(0, $this->getExitCode());
+
+ $expectedFile = $this->testOutputDir . '/UserManagerCommand.php';
+ $content = file_get_contents($expectedFile);
+
+ $this->assertStringContainsString('createRecord()', $content);
+ $this->assertStringContainsString('updateRecord()', $content);
+ $this->assertStringContainsString('deleteRecord()', $content);
+ $this->assertStringContainsString('listRecords()', $content);
+ }
+
+ /**
+ * Test command generation with arguments.
+ *
+ * @test
+ */
+ public function testCommandWithArguments() {
+ $command = new MakeCommand();
+
+ $output = $this->executeSingleCommand($command, [
+ '--name' => 'process-data',
+ '--args' => 'input file,output format,verbose mode',
+ '--path' => $this->testOutputDir
+ ], ['']); // Empty namespace input
+
+ $this->assertEquals(0, $this->getExitCode());
+
+ $expectedFile = $this->testOutputDir . '/ProcessDataCommand.php';
+ $content = file_get_contents($expectedFile);
+
+ $this->assertStringContainsString('--input-file', $content);
+ $this->assertStringContainsString('--output-format', $content);
+ $this->assertStringContainsString('--verbose-mode', $content);
+ }
+
+ /**
+ * Test invalid command name validation.
+ *
+ * @test
+ */
+ public function testInvalidCommandName() {
+ $command = new MakeCommand();
+
+ $output = $this->executeSingleCommand($command, [
+ '--name' => 'Invalid Command Name!',
+ '--path' => $this->testOutputDir
+ ], ['']); // Empty namespace input
+
+ $this->assertEquals(1, $this->getExitCode());
+ // Check for validation error message (flexible matching)
+ $found = false;
+ foreach ($output as $line) {
+ if (strpos($line, 'Command name must start with a letter') !== false) {
+ $found = true;
+ break;
+ }
+ }
+ $this->assertTrue($found, 'Validation error message not found in output');
+ }
+
+ /**
+ * Test file overwrite confirmation.
+ *
+ * @test
+ */
+ public function testFileOverwriteConfirmation() {
+ $command = new MakeCommand();
+
+ // Create file first
+ $this->executeSingleCommand($command, [
+ '--name' => 'existing-command',
+ '--path' => $this->testOutputDir
+ ], ['']); // Empty namespace input
+
+ // Try to create again with 'no' confirmation
+ $output = $this->executeSingleCommand($command, [
+ '--name' => 'existing-command',
+ '--path' => $this->testOutputDir
+ ], ['', 'n']); // Empty namespace, then 'no' to overwrite
+
+ $this->assertEquals(1, $this->getExitCode());
+ // Check for overwrite declined message (flexible matching)
+ $found = false;
+ foreach ($output as $line) {
+ if (strpos($line, 'File already exists and overwrite was declined') !== false) {
+ $found = true;
+ break;
+ }
+ }
+ $this->assertTrue($found, 'Overwrite declined message not found in output');
+ }
+
+ /**
+ * Helper method to remove directory recursively.
+ */
+ private function removeDirectory(string $dir): void {
+ if (!is_dir($dir)) {
+ return;
+ }
+
+ $files = array_diff(scandir($dir), ['.', '..']);
+ foreach ($files as $file) {
+ $path = $dir . '/' . $file;
+ is_dir($path) ? $this->removeDirectory($path) : unlink($path);
+ }
+ rmdir($dir);
+ }
+}
diff --git a/tests/WebFiori/Tests/Cli/MaskedInputTest.php b/tests/WebFiori/Tests/Cli/MaskedInputTest.php
new file mode 100644
index 0000000..6061bf4
--- /dev/null
+++ b/tests/WebFiori/Tests/Cli/MaskedInputTest.php
@@ -0,0 +1,190 @@
+getMaskedInput('Enter password: ');
+ $this->println("Password received: $input");
+ return 0;
+ }
+ };
+
+ $output = $this->executeSingleCommand($command, [], ['secret123']);
+
+ $this->assertContains("Enter password:\n", $output);
+ $this->assertContains("Password received: secret123\n", $output);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * Test masked input with default value.
+ *
+ * @test
+ */
+ public function testMaskedInputWithDefault() {
+ $command = new class extends \WebFiori\Cli\Command {
+ public function __construct() {
+ parent::__construct('test-masked-default');
+ }
+
+ public function exec(): int {
+ $input = $this->getMaskedInput('Enter token: ', '*', 'default-token');
+ $this->println("Token: $input");
+ return 0;
+ }
+ };
+
+ // Test with empty input (should use default)
+ $output = $this->executeSingleCommand($command, [], ['']);
+
+ $this->assertContains("Enter token: Enter = 'default-token'\n", $output);
+ $this->assertContains("Token: default-token\n", $output);
+ }
+
+ /**
+ * Test masked input with custom mask character.
+ *
+ * @test
+ */
+ public function testMaskedInputWithCustomMask() {
+ $command = new class extends \WebFiori\Cli\Command {
+ public function __construct() {
+ parent::__construct('test-custom-mask');
+ }
+
+ public function exec(): int {
+ $input = $this->getMaskedInput('Enter PIN: ', '#');
+ $this->println("PIN: $input");
+ return 0;
+ }
+ };
+
+ $output = $this->executeSingleCommand($command, [], ['1234']);
+
+ $this->assertContains("Enter PIN:\n", $output);
+ $this->assertContains("PIN: 1234\n", $output);
+ }
+
+ /**
+ * Test masked input with validation.
+ *
+ * @test
+ */
+ public function testMaskedInputWithValidation() {
+ $command = new class extends \WebFiori\Cli\Command {
+ public function __construct() {
+ parent::__construct('test-masked-validation');
+ }
+
+ public function exec(): int {
+ $validator = new InputValidator(function($input) {
+ return strlen($input) >= 8;
+ }, 'Password must be at least 8 characters long!');
+
+ $input = $this->getMaskedInput('Enter password: ', '*', null, $validator);
+ $this->println("Valid password received");
+ return 0;
+ }
+ };
+
+ // Test with invalid input first, then valid
+ $output = $this->executeSingleCommand($command, [], ['short', 'validpassword']);
+
+ $this->assertContains("Error: Password must be at least 8 characters long!\n", $output);
+ $this->assertContains("Valid password received\n", $output);
+ }
+
+ /**
+ * Test masked input with empty prompt.
+ *
+ * @test
+ */
+ public function testMaskedInputWithEmptyPrompt() {
+ $command = new class extends \WebFiori\Cli\Command {
+ public function __construct() {
+ parent::__construct('test-empty-prompt');
+ }
+
+ public function exec(): int {
+ $input = $this->getMaskedInput('');
+ $result = $input === null ? 'null' : $input;
+ $this->println("Result: $result");
+ return 0;
+ }
+ };
+
+ $output = $this->executeSingleCommand($command);
+
+ $this->assertContains("Result: null\n", $output);
+ }
+
+ /**
+ * Test masked input with whitespace handling.
+ *
+ * @test
+ */
+ public function testMaskedInputWhitespaceHandling() {
+ $command = new class extends \WebFiori\Cli\Command {
+ public function __construct() {
+ parent::__construct('test-whitespace');
+ }
+
+ public function exec(): int {
+ $input = $this->getMaskedInput('Enter value: ');
+ $this->println("Value: '$input'");
+ return 0;
+ }
+ };
+
+ // Test with leading/trailing spaces
+ $output = $this->executeSingleCommand($command, [], [' spaced ']);
+
+ $this->assertContains("Value: 'spaced'\n", $output); // Should be trimmed
+ }
+
+ /**
+ * Test masked input with special characters.
+ *
+ * @test
+ */
+ public function testMaskedInputWithSpecialCharacters() {
+ $command = new class extends \WebFiori\Cli\Command {
+ public function __construct() {
+ parent::__construct('test-special-chars');
+ }
+
+ public function exec(): int {
+ $input = $this->getMaskedInput('Enter complex password: ');
+ $this->println("Password length: " . strlen($input));
+ return 0;
+ }
+ };
+
+ $complexPassword = 'P@ssw0rd!#$%';
+ $output = $this->executeSingleCommand($command, [], [$complexPassword]);
+
+ $this->assertContains("Password length: " . strlen($complexPassword) . "\n", $output);
+ }
+}