• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valksor / php-dev-snapshot / 19705546221

26 Nov 2025 01:33PM UTC coverage: 49.15% (-12.4%) from 61.538%
19705546221

push

github

k0d3r1s
generic binary provider

376 of 765 relevant lines covered (49.15%)

2.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.85
/Command/SnapshotGenerateCommand.php
1
<?php declare(strict_types = 1);
2

3
/*
4
 * This file is part of the Valksor package.
5
 *
6
 * (c) Davis Zalitis (k0d3r1s)
7
 * (c) SIA Valksor <packages@valksor.com>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12

13
namespace ValksorDev\Snapshot\Command;
14

15
use Symfony\Component\Console\Attribute\AsCommand;
16
use Symfony\Component\Console\Input\InputArgument;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Input\InputOption;
19
use Symfony\Component\Console\Output\OutputInterface;
20
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
21
use Valksor\Bundle\Command\AbstractCommand;
22
use ValksorDev\Snapshot\Service\SnapshotService;
23

24
use function array_map;
25
use function explode;
26
use function getcwd;
27
use function is_array;
28
use function is_dir;
29
use function max;
30
use function realpath;
31
use function str_contains;
32
use function substr;
33
use function trim;
34

35
use const PHP_OS;
36

37
/**
38
 * Console command for generating MCP (Markdown Context Pack) snapshots.
39
 *
40
 * This command creates AI-optimized project documentation snapshots that include
41
 * project structure, file contents organized by language, and comprehensive
42
 * statistics. The output format is specifically designed for consumption by
43
 * AI assistants and code analysis tools.
44
 *
45
 * Key Features:
46
 * - Multi-path scanning with flexible configuration
47
 * - Intelligent file filtering with binary detection
48
 * - Gitignore integration for smart filtering
49
 * - Configurable limits for file size, count, and lines
50
 * - Extension allowlisting and custom ignore patterns
51
 * - MCP format output optimized for AI consumption
52
 *
53
 * Use Cases:
54
 * - AI code analysis and review
55
 * - Project documentation generation
56
 * - Code base summarization for context
57
 * - Knowledge base creation for assistants
58
 */
59
#[AsCommand(
60
    name: 'valksor:snapshot',
61
    description: 'Generate project snapshots in MCP format for AI consumption.',
62
)]
63
final class SnapshotGenerateCommand extends AbstractCommand
64
{
65
    public function __construct(
66
        ParameterBagInterface $parameterBag,
67
        private readonly SnapshotService $snapshotService,
68
    ) {
69
        parent::__construct($parameterBag);
5✔
70
    }
71

72
    /**
73
     * Execute the snapshot generation command.
74
     *
75
     * Processes command arguments and options, configures the snapshot service,
76
     * and triggers the snapshot generation with proper error handling.
77
     */
78
    public function __invoke(
79
        InputInterface $input,
80
        OutputInterface $output,
81
    ): int {
82
        $io = $this->createSymfonyStyle($input, $output);
4✔
83
        $this->snapshotService->setIo($io);
4✔
84

85
        // Validate and prepare paths
86
        $paths = $this->preparePaths($input, $io);
4✔
87

88
        if (empty($paths)) {
4✔
89
            return 1; // Error already shown in preparePaths
×
90
        }
91

92
        // Build configuration
93
        $config = $this->buildConfig($input, $paths);
4✔
94

95
        // Generate snapshot
96
        return $this->snapshotService->start($config);
4✔
97
    }
98

99
    /**
100
     * Configure command arguments and options.
101
     *
102
     * Sets up comprehensive configuration options for controlling snapshot
103
     * generation including paths, filtering, output format, and limits.
104
     */
105
    protected function configure(): void
106
    {
107
        $this
5✔
108
            ->addArgument(
5✔
109
                'paths',
5✔
110
                InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
5✔
111
                'Path(s) to scan (can specify multiple paths)',
5✔
112
                [getcwd()],
5✔
113
            )
5✔
114
            ->addOption(
5✔
115
                'output',
5✔
116
                'o',
5✔
117
                InputOption::VALUE_REQUIRED,
5✔
118
                'Output file name (auto-generated with timestamp if not specified)',
5✔
119
            )
5✔
120
            ->addOption(
5✔
121
                'no-gitignore',
5✔
122
                null,
5✔
123
                InputOption::VALUE_NONE,
5✔
124
                'Ignore .gitignore patterns and process all files',
5✔
125
            )
5✔
126
            ->addOption(
5✔
127
                'include-vendors',
5✔
128
                null,
5✔
129
                InputOption::VALUE_NONE,
5✔
130
                'Include vendor directories (node_modules, vendor, etc.)',
5✔
131
            )
5✔
132
            ->addOption(
5✔
133
                'include-hidden',
5✔
134
                null,
5✔
135
                InputOption::VALUE_NONE,
5✔
136
                'Include hidden files and directories (starting with .)',
5✔
137
            )
5✔
138
            ->addOption(
5✔
139
                'max-files',
5✔
140
                null,
5✔
141
                InputOption::VALUE_REQUIRED,
5✔
142
                'Maximum number of files to process (0 for unlimited)',
5✔
143
                '500',
5✔
144
            )
5✔
145
            ->addOption(
5✔
146
                'max-size',
5✔
147
                null,
5✔
148
                InputOption::VALUE_REQUIRED,
5✔
149
                'Maximum file size in KB (0 for unlimited)',
5✔
150
                '1024',
5✔
151
            )
5✔
152
            ->addOption(
5✔
153
                'max-lines',
5✔
154
                null,
5✔
155
                InputOption::VALUE_REQUIRED,
5✔
156
                'Maximum lines per file (0 for unlimited)',
5✔
157
                '1000',
5✔
158
            )
5✔
159
            ->addOption(
5✔
160
                'extensions',
5✔
161
                'ext',
5✔
162
                InputOption::VALUE_REQUIRED,
5✔
163
                'Only include files with these extensions (comma-separated, no dots)',
5✔
164
            )
5✔
165
            ->addOption(
5✔
166
                'ignore',
5✔
167
                null,
5✔
168
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
5✔
169
                'Files/directories/extensions to ignore (can specify multiple times)',
5✔
170
            )
5✔
171
            ->setHelp(
5✔
172
                <<<'EOF'
5✔
173
                    The <info>%command.name%</info> command generates project snapshots in MCP (Markdown Context Pack) format optimized for AI consumption.
174

175
                    <info>Usage examples:</info>
176

177
                    Generate snapshot of current directory:
178
                      <info>php %command.full_name%</info>
179

180
                    Generate with custom output file:
181
                      <info>php %command.full_name% --output=project-snapshot.mcp</info>
182

183
                    Scan multiple specific directories:
184
                      <info>php %command.full_name% src/ config/ docs/</info>
185

186
                    Include everything (ignore gitignore and vendors):
187
                      <info>php %command.full_name% --no-gitignore --include-vendors</info>
188

189
                    Only process PHP and JavaScript files:
190
                      <info>php %command.full_name% --extensions=php,javascript</info>
191

192
                    Ignore specific patterns:
193
                      <info>php %command.full_name% --ignore="*.log" --ignore="temp/" --ignore="cache"</info>
194

195
                    Advanced filtering with limits:
196
                      <info>php %command.full_name% --max-files=1000 --max-size=2048 --max-lines=2000</info>
197

198
                    <info>Output format:</info>
199
                      The command generates MCP (Markdown Context Pack) format that includes:
200
                      • Project metadata in JSON format
201
                      • Directory structure tree visualization
202
                      • File contents grouped by programming language
203
                      • Comprehensive statistics and breakdown
204
                      • Syntax highlighting for code blocks
205

206
                    <info>Default behavior:</info>
207
                      • Excludes vendor/, node_modules/, .git/, cache, logs, binaries
208
                      • Includes source code, config files, documentation
209
                      • Respects .gitignore patterns (can be overridden with --no-gitignore)
210
                      • Limits file size to 1MB and file count to 500 (configurable)
211
                      • Automatically detects file types and programming languages
212
                    EOF
5✔
213
            );
5✔
214
    }
215

216
    /**
217
     * Build configuration array for the snapshot service.
218
     *
219
     * Processes all command options and converts them into the configuration
220
     * format expected by the SnapshotService.
221
     *
222
     * @param array<string> $paths Array of valid paths to process
223
     */
224
    private function buildConfig(
225
        InputInterface $input,
226
        array $paths,
227
    ): array {
228
        $config = [
4✔
229
            'paths' => $paths,
4✔
230
            'output_file' => $input->getOption('output') ?? 'snapshots/snapshot.mcp',
4✔
231
            'no_gitignore' => $input->getOption('no-gitignore'),
4✔
232
            'include_vendors' => $input->getOption('include-vendors'),
4✔
233
            'include_hidden' => $input->getOption('include-hidden'),
4✔
234
            'strip_comments' => true,
4✔
235
        ];
4✔
236

237
        // Convert numeric options with proper validation
238
        $maxFiles = $this->parseNumericOption($input->getOption('max-files'));
4✔
239
        $maxSize = $this->parseNumericOption($input->getOption('max-size'));
4✔
240
        $maxLines = $this->parseNumericOption($input->getOption('max-lines'));
4✔
241

242
        $config['max_files'] = max($maxFiles, 0);
4✔
243
        $config['max_file_size'] = max($maxSize, 0);
4✔
244
        $config['max_lines'] = max($maxLines, 0);
4✔
245

246
        // Process extensions option
247
        $extensions = $input->getOption('extensions');
4✔
248

249
        if (!empty($extensions)) {
4✔
250
            $config['allowed_extensions'] = $this->parseExtensions($extensions);
×
251
        }
252

253
        // Process ignore patterns
254
        $ignorePatterns = $input->getOption('ignore');
4✔
255

256
        if (!empty($ignorePatterns)) {
4✔
257
            $config['ignore_patterns'] = $this->parseIgnorePatterns($ignorePatterns);
×
258
        }
259

260
        return $config;
4✔
261
    }
262

263
    /**
264
     * Check if a path is absolute.
265
     */
266
    private function isAbsolutePath(
267
        string $path,
268
    ): bool {
269
        return str_starts_with($path, '/') || (PHP_OS === 'WINNT' && str_contains($path, ':'));
4✔
270
    }
271

272
    /**
273
     * Parse comma-separated extensions into an array.
274
     */
275
    private function parseExtensions(
276
        string $extensions,
277
    ): array {
278
        return array_map('trim', explode(',', $extensions));
×
279
    }
280

281
    /**
282
     * Parse ignore patterns into structured array.
283
     */
284
    private function parseIgnorePatterns(
285
        array $patterns,
286
    ): array {
287
        $structured = [
×
288
            'files' => [],
×
289
            'dirs' => [],
×
290
            'extensions' => [],
×
291
            'paths' => [],
×
292
        ];
×
293

294
        foreach ($patterns as $pattern) {
×
295
            if (empty($pattern)) {
×
296
                continue;
×
297
            }
298

299
            $pattern = trim($pattern);
×
300

301
            // Directory pattern (ends with /)
302
            if (str_ends_with($pattern, '/')) {
×
303
                $structured['dirs'][] = $pattern;
×
304
            }
305
            // Extension pattern (starts with *. or contains .)
306
            elseif (str_starts_with($pattern, '*.') && !str_contains($pattern, '/')) {
×
307
                $structured['extensions'][] = substr($pattern, 2);
×
308
            }
309
            // Path pattern (contains /)
310
            elseif (str_contains($pattern, '/')) {
×
311
                $structured['paths'][] = $pattern;
×
312
            }
313
            // File pattern (simple filename)
314
            else {
315
                $structured['files'][] = $pattern;
×
316
            }
317
        }
318

319
        return $structured;
×
320
    }
321

322
    /**
323
     * Parse numeric option with validation.
324
     */
325
    private function parseNumericOption(
326
        $value,
327
    ): int {
328
        if (is_array($value)) {
4✔
329
            $value = $value[0] ?? 0;
×
330
        }
331

332
        return (int) $value;
4✔
333
    }
334

335
    /**
336
     * Prepare and validate paths from command input.
337
     *
338
     * Processes the paths argument, validates that they exist and are directories,
339
     * and returns an array of valid absolute paths.
340
     *
341
     * @return array<string> Array of valid absolute paths
342
     */
343
    private function preparePaths(
344
        InputInterface $input,
345
        $io,
346
    ): array {
347
        $paths = $input->getArgument('paths');
4✔
348
        $validPaths = [];
4✔
349

350
        foreach ($paths as $path) {
4✔
351
            // Convert to absolute path
352
            if (!$this->isAbsolutePath($path)) {
4✔
353
                $path = getcwd() . '/' . $path;
×
354
            }
355

356
            // Validate path exists and is a directory
357
            if (!is_dir($path)) {
4✔
358
                $io->warning("Path does not exist or is not a directory: $path");
×
359

360
                continue;
×
361
            }
362

363
            // Convert to real path for consistency
364
            $realPath = realpath($path);
4✔
365

366
            if (false !== $realPath) {
4✔
367
                $validPaths[] = $realPath;
4✔
368
            }
369
        }
370

371
        if (empty($validPaths)) {
4✔
372
            $io->error('No valid paths found to process.');
×
373
        }
374

375
        return $validPaths;
4✔
376
    }
377
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc