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

valksor / php-dev-snapshot / 19634210989

24 Nov 2025 12:21PM UTC coverage: 61.538%. First build
19634210989

push

github

k0d3r1s
add valksor-dev snapshot

336 of 546 new or added lines in 4 files covered. (61.54%)

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 explode;
25
use function getcwd;
26
use function is_array;
27
use function is_dir;
28
use function realpath;
29
use function str_contains;
30
use function trim;
31

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

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

80
        // Validate and prepare paths
81
        $paths = $this->preparePaths($input, $io);
4✔
82

83
        if (empty($paths)) {
4✔
NEW
84
            return 1; // Error already shown in preparePaths
×
85
        }
86

87
        // Build configuration
88
        $config = $this->buildConfig($input, $paths);
4✔
89

90
        // Generate snapshot
91
        return $this->snapshotService->start($config);
4✔
92
    }
93

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

170
                    <info>Usage examples:</info>
171

172
                    Generate snapshot of current directory:
173
                      <info>php %command.full_name%</info>
174

175
                    Generate with custom output file:
176
                      <info>php %command.full_name% --output=project-snapshot.mcp</info>
177

178
                    Scan multiple specific directories:
179
                      <info>php %command.full_name% src/ config/ docs/</info>
180

181
                    Include everything (ignore gitignore and vendors):
182
                      <info>php %command.full_name% --no-gitignore --include-vendors</info>
183

184
                    Only process PHP and JavaScript files:
185
                      <info>php %command.full_name% --extensions=php,javascript</info>
186

187
                    Ignore specific patterns:
188
                      <info>php %command.full_name% --ignore="*.log" --ignore="temp/" --ignore="cache"</info>
189

190
                    Advanced filtering with limits:
191
                      <info>php %command.full_name% --max-files=1000 --max-size=2048 --max-lines=2000</info>
192

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

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

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

231
        // Convert numeric options with proper validation
232
        $maxFiles = $this->parseNumericOption($input->getOption('max-files'));
4✔
233
        $maxSize = $this->parseNumericOption($input->getOption('max-size'));
4✔
234
        $maxLines = $this->parseNumericOption($input->getOption('max-lines'));
4✔
235

236
        $config['max_files'] = max($maxFiles, 0);
4✔
237
        $config['max_file_size'] = max($maxSize, 0);
4✔
238
        $config['max_lines'] = max($maxLines, 0);
4✔
239

240
        // Process extensions option
241
        $extensions = $input->getOption('extensions');
4✔
242

243
        if (!empty($extensions)) {
4✔
NEW
244
            $config['allowed_extensions'] = $this->parseExtensions($extensions);
×
245
        }
246

247
        // Process ignore patterns
248
        $ignorePatterns = $input->getOption('ignore');
4✔
249

250
        if (!empty($ignorePatterns)) {
4✔
NEW
251
            $config['ignore_patterns'] = $this->parseIgnorePatterns($ignorePatterns);
×
252
        }
253

254
        return $config;
4✔
255
    }
256

257
    /**
258
     * Check if a path is absolute.
259
     */
260
    private function isAbsolutePath(
261
        string $path,
262
    ): bool {
263
        return str_starts_with($path, '/') || (PHP_OS === 'WINNT' && str_contains($path, ':'));
4✔
264
    }
265

266
    /**
267
     * Parse comma-separated extensions into an array.
268
     */
269
    private function parseExtensions(
270
        string $extensions,
271
    ): array {
NEW
272
        return array_map('trim', explode(',', $extensions));
×
273
    }
274

275
    /**
276
     * Parse ignore patterns into structured array.
277
     */
278
    private function parseIgnorePatterns(
279
        array $patterns,
280
    ): array {
NEW
281
        $structured = [
×
NEW
282
            'files' => [],
×
NEW
283
            'dirs' => [],
×
NEW
284
            'extensions' => [],
×
NEW
285
            'paths' => [],
×
NEW
286
        ];
×
287

NEW
288
        foreach ($patterns as $pattern) {
×
NEW
289
            if (empty($pattern)) {
×
NEW
290
                continue;
×
291
            }
292

NEW
293
            $pattern = trim($pattern);
×
294

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

NEW
313
        return $structured;
×
314
    }
315

316
    /**
317
     * Parse numeric option with validation.
318
     */
319
    private function parseNumericOption(
320
        $value,
321
    ): int {
322
        if (is_array($value)) {
4✔
NEW
323
            $value = $value[0] ?? 0;
×
324
        }
325

326
        return (int) $value;
4✔
327
    }
328

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

344
        foreach ($paths as $path) {
4✔
345
            // Convert to absolute path
346
            if (!$this->isAbsolutePath($path)) {
4✔
NEW
347
                $path = getcwd() . '/' . $path;
×
348
            }
349

350
            // Validate path exists and is a directory
351
            if (!is_dir($path)) {
4✔
NEW
352
                $io->warning("Path does not exist or is not a directory: $path");
×
353

NEW
354
                continue;
×
355
            }
356

357
            // Convert to real path for consistency
358
            $realPath = realpath($path);
4✔
359

360
            if (false !== $realPath) {
4✔
361
                $validPaths[] = $realPath;
4✔
362
            }
363
        }
364

365
        if (empty($validPaths)) {
4✔
NEW
366
            $io->error('No valid paths found to process.');
×
367
        }
368

369
        return $validPaths;
4✔
370
    }
371
}
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