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

valksor / php-dev-snapshot / 19641329740

24 Nov 2025 03:43PM UTC coverage: 61.538%. Remained the same
19641329740

push

github

k0d3r1s
strip snapshot

8 of 11 new or added lines in 3 files covered. (72.73%)

67 existing lines in 3 files now uncovered.

336 of 546 relevant lines covered (61.54%)

2.88 hits per line

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

79.7
/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
{
5✔
65
    public function __construct(
66
        ParameterBagInterface $parameterBag,
67
        private readonly SnapshotService $snapshotService,
68
    ) {
69
        parent::__construct($parameterBag);
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
     */
4✔
78
    public function __invoke(
4✔
79
        InputInterface $input,
80
        OutputInterface $output,
81
    ): int {
4✔
82
        $io = $this->createSymfonyStyle($input, $output);
83
        $this->snapshotService->setIo($io);
4✔
UNCOV
84

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

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

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

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

99
    /**
100
     * Configure command arguments and options.
101
     *
102
     * Sets up comprehensive configuration options for controlling snapshot
5✔
103
     * generation including paths, filtering, output format, and limits.
5✔
104
     */
5✔
105
    protected function configure(): void
5✔
106
    {
5✔
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,
169
                'Files/directories/extensions to ignore (can specify multiple times)',
170
            )
171
            ->setHelp(
172
                <<<'EOF'
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
5✔
208
                      • Includes source code, config files, documentation
5✔
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
213
            );
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
     */
4✔
224
    private function buildConfig(
4✔
225
        InputInterface $input,
4✔
226
        array $paths,
4✔
227
    ): array {
4✔
228
        $config = [
4✔
229
            'paths' => $paths,
4✔
230
            'output_file' => $input->getOption('output') ?? 'snapshots/snapshot.mcp',
231
            'no_gitignore' => $input->getOption('no-gitignore'),
232
            'include_vendors' => $input->getOption('include-vendors'),
4✔
233
            'include_hidden' => $input->getOption('include-hidden'),
4✔
234
            'strip_comments' => true,
4✔
235
        ];
236

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

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

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

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

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

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

260
        return $config;
261
    }
262

263
    /**
4✔
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, ':'));
270
    }
271

UNCOV
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

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

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

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

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

319
        return $structured;
320
    }
321

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

332
        return (int) $value;
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
4✔
342
     */
4✔
343
    private function preparePaths(
344
        InputInterface $input,
4✔
345
        $io,
346
    ): array {
4✔
UNCOV
347
        $paths = $input->getArgument('paths');
×
348
        $validPaths = [];
349

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

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

360
                continue;
4✔
361
            }
4✔
362

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

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

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

375
        return $validPaths;
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