Skip to content

Commit fa6a8f5

Browse files
committed
feat: implement template system with smart project analysis
Add intelligent template-based initialization system for CTX with automatic project detection and fallback handling.
1 parent de482a7 commit fa6a8f5

24 files changed

+2055
-33
lines changed

context.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ documents:
2424
- '*Interface.php'
2525
showTreeView: true
2626

27+
- description: Project Templates
28+
outputPath: core/templates.md
29+
sources:
30+
- type: file
31+
sourcePaths:
32+
- src/Template
33+
- vendor/spiral/files/src/FilesInterface.php
34+
showTreeView: true
35+
2736
- description: "Changes in the Project"
2837
outputPath: "changes.md"
2938
sources:

docs/template-system.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Template System Usage
2+
3+
The CTX template system provides intelligent project analysis and template-based configuration generation.
4+
5+
## Commands
6+
7+
### List Templates
8+
```bash
9+
# Show all available templates
10+
ctx template:list
11+
12+
# Show detailed template information
13+
ctx template:list --detailed
14+
15+
# Filter by tags
16+
ctx template:list --tag php --tag laravel
17+
18+
# Combine options
19+
ctx template:list --detailed --tag php
20+
```
21+
22+
### Initialize with Template
23+
```bash
24+
# Auto-detect project type and use appropriate template
25+
ctx init
26+
27+
# Use specific template
28+
ctx init laravel
29+
ctx init generic-php
30+
31+
# Specify custom config filename
32+
ctx init laravel --config-file=custom-context.yaml
33+
```
34+
35+
## Available Templates
36+
37+
### Laravel Template (`laravel`)
38+
- **Priority**: 100 (high)
39+
- **Tags**: php, laravel, web, framework
40+
- **Detection**: Looks for `composer.json`, `artisan` file, and Laravel directories
41+
- **Generated Documents**:
42+
- `docs/laravel-overview.md` - Application overview
43+
- `docs/laravel-structure.md` - Directory structure with tree view
44+
45+
### Generic PHP Template (`generic-php`)
46+
- **Priority**: 10 (low)
47+
- **Tags**: php, generic
48+
- **Detection**: Looks for `composer.json` and `src` directory
49+
- **Generated Documents**:
50+
- `docs/php-overview.md` - Project overview
51+
- `docs/php-structure.md` - Directory structure with tree view
52+
53+
## Analysis Process
54+
55+
When you run `ctx init` without specifying a template:
56+
57+
1. **Project Analysis**: Scans the current directory for project indicators
58+
2. **Template Matching**: Matches detected patterns to available templates
59+
3. **Confidence Scoring**: Calculates confidence levels for matches
60+
4. **Auto-Selection**: Uses high-confidence matches automatically
61+
5. **Manual Selection**: Shows options for lower-confidence matches
62+
63+
### Analysis Output Example
64+
```
65+
$ ctx init
66+
Analyzing project...
67+
✅ Detected: laravel (confidence: 95%)
68+
Using template: Laravel PHP Framework project template
69+
✅ Config context.yaml created
70+
```
71+
72+
## Extending the System
73+
74+
The template system is designed to be extensible:
75+
76+
- **Custom Analyzers**: Implement `ProjectAnalyzerInterface`
77+
- **Custom Templates**: Implement `TemplateProviderInterface`
78+
- **Custom Template Sources**: File-based, remote, or database templates
79+
80+
Templates automatically integrate with the existing CTX configuration system, supporting all source types, modifiers, and features.

src/Application/Kernel.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Butschster\ContextGenerator\Application\Bootloader\SourceFetcherBootloader;
2222
use Butschster\ContextGenerator\Application\Bootloader\VariableBootloader;
2323
use Butschster\ContextGenerator\McpServer\McpServerBootloader;
24+
use Butschster\ContextGenerator\Template\TemplateSystemBootloader;
2425
use Butschster\ContextGenerator\Modifier\PhpContentFilter\PhpContentFilterBootloader;
2526
use Butschster\ContextGenerator\Modifier\PhpDocs\PhpDocsModifierBootloader;
2627
use Butschster\ContextGenerator\Modifier\PhpSignature\PhpSignatureModifierBootloader;
@@ -70,6 +71,9 @@ protected function defineBootloaders(): array
7071
SourceRegistryBootloader::class,
7172
SchemaMapperBootloader::class,
7273

74+
// Template System
75+
TemplateSystemBootloader::class,
76+
7377
// Sources
7478
TextSourceBootloader::class,
7579
FileSourceBootloader::class,

src/Console/InitCommand.php

Lines changed: 147 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@
44

55
namespace Butschster\ContextGenerator\Console;
66

7-
use Butschster\ContextGenerator\Application\JsonSchema;
87
use Butschster\ContextGenerator\Config\ConfigType;
98
use Butschster\ContextGenerator\Config\Registry\ConfigRegistry;
109
use Butschster\ContextGenerator\DirectoriesInterface;
11-
use Butschster\ContextGenerator\Document\Document;
12-
use Butschster\ContextGenerator\Document\DocumentRegistry;
13-
use Butschster\ContextGenerator\Lib\TreeBuilder\TreeViewConfig;
14-
use Butschster\ContextGenerator\Source\Tree\TreeSource;
10+
use Butschster\ContextGenerator\Template\Analysis\ProjectAnalysisService;
11+
use Butschster\ContextGenerator\Template\Registry\TemplateRegistry;
12+
use Spiral\Console\Attribute\Argument;
1513
use Spiral\Console\Attribute\Option;
1614
use Spiral\Files\FilesInterface;
1715
use Symfony\Component\Console\Attribute\AsCommand;
@@ -20,27 +18,36 @@
2018

2119
#[AsCommand(
2220
name: 'init',
23-
description: 'Initialize a new context configuration file',
21+
description: 'Initialize a new context configuration file with smart project analysis',
2422
)]
2523
final class InitCommand extends BaseCommand
2624
{
25+
#[Argument(
26+
name: 'template',
27+
description: 'Specific template to use (optional)',
28+
)]
29+
protected ?string $template = null;
30+
2731
#[Option(
2832
name: 'config-file',
2933
shortcut: 'c',
3034
description: 'The name of the file to create',
3135
)]
3236
protected string $configFilename = 'context.yaml';
3337

34-
public function __invoke(DirectoriesInterface $dirs, FilesInterface $files): int
35-
{
38+
public function __invoke(
39+
DirectoriesInterface $dirs,
40+
FilesInterface $files,
41+
TemplateRegistry $templateRegistry,
42+
ProjectAnalysisService $analysisService,
43+
): int {
3644
$filename = $this->configFilename;
37-
$ext = \pathinfo($filename, PATHINFO_EXTENSION);
45+
$ext = \pathinfo($filename, \PATHINFO_EXTENSION);
3846

3947
try {
4048
$type = ConfigType::fromExtension($ext);
4149
} catch (\ValueError) {
4250
$this->output->error(\sprintf('Unsupported config type: %s', $ext));
43-
4451
return Command::FAILURE;
4552
}
4653

@@ -49,27 +56,141 @@ public function __invoke(DirectoriesInterface $dirs, FilesInterface $files): int
4956

5057
if ($files->exists($filePath)) {
5158
$this->output->error(\sprintf('Config %s already exists', $filePath));
59+
return Command::FAILURE;
60+
}
61+
62+
if ($this->template !== null) {
63+
return $this->initWithTemplate($files, $templateRegistry, $this->template, $type, $filePath);
64+
}
65+
66+
return $this->initWithAnalysis($dirs, $files, $analysisService, $templateRegistry, $type, $filePath);
67+
}
68+
69+
private function initWithTemplate(
70+
FilesInterface $files,
71+
TemplateRegistry $templateRegistry,
72+
string $templateName,
73+
ConfigType $type,
74+
string $filePath,
75+
): int {
76+
$template = $templateRegistry->getTemplate($templateName);
77+
78+
if ($template === null) {
79+
$this->output->error(\sprintf('Template "%s" not found', $templateName));
80+
81+
$this->output->note('Available templates:');
82+
foreach ($templateRegistry->getAllTemplates() as $availableTemplate) {
83+
$this->output->writeln(\sprintf(' - %s: %s', $availableTemplate->name, $availableTemplate->description));
84+
}
85+
86+
$this->output->writeln('');
87+
$this->output->writeln('Use <info>ctx template:list</info> to see all available templates with details.');
5288

5389
return Command::FAILURE;
5490
}
5591

56-
$config = new ConfigRegistry(
57-
schema: JsonSchema::SCHEMA_URL,
58-
);
59-
60-
$config->register(new DocumentRegistry([
61-
new Document(
62-
description: 'Project structure overview',
63-
outputPath: 'project-structure.md',
64-
firstSource: new TreeSource(
65-
sourcePaths: ['src'],
66-
treeView: new TreeViewConfig(
67-
showCharCount: true,
68-
),
69-
),
70-
),
71-
]));
92+
$this->output->success(\sprintf('Using template: %s', $template->description));
93+
94+
return $this->writeConfig($files, $template->config, $type, $filePath);
95+
}
96+
97+
private function initWithAnalysis(
98+
DirectoriesInterface $dirs,
99+
FilesInterface $files,
100+
ProjectAnalysisService $analysisService,
101+
TemplateRegistry $templateRegistry,
102+
ConfigType $type,
103+
string $filePath,
104+
): int {
105+
$this->output->writeln('Analyzing project...');
106+
107+
$results = $analysisService->analyzeProject($dirs->getRootPath());
108+
$bestMatch = $results[0];
109+
110+
// Check if this is a fallback result
111+
$isFallback = $bestMatch->metadata['isFallback'] ?? false;
112+
113+
if ($isFallback) {
114+
$this->output->warning('No specific project type detected. Using default configuration.');
115+
} else {
116+
$this->output->success(\sprintf(
117+
'Detected: %s (confidence: %.0f%%)',
118+
$bestMatch->detectedType,
119+
$bestMatch->confidence * 100,
120+
));
121+
}
72122

123+
if ($bestMatch->hasHighConfidence()) {
124+
$primaryTemplate = $bestMatch->getPrimaryTemplate();
125+
if ($primaryTemplate !== null) {
126+
$template = $templateRegistry->getTemplate($primaryTemplate);
127+
if ($template !== null) {
128+
$this->output->writeln(\sprintf('Using template: %s', $template->description));
129+
return $this->writeConfig($files, $template->config, $type, $filePath);
130+
}
131+
}
132+
}
133+
134+
// Show analysis results for lower confidence matches
135+
return $this->showAnalysisResults($results, $templateRegistry, $files, $type, $filePath);
136+
}
137+
138+
private function showAnalysisResults(
139+
array $results,
140+
TemplateRegistry $templateRegistry,
141+
FilesInterface $files,
142+
ConfigType $type,
143+
string $filePath,
144+
): int {
145+
$this->output->title('Analysis Results');
146+
147+
foreach ($results as $result) {
148+
// Skip showing fallback results in detailed analysis
149+
if ($result->metadata['isFallback'] ?? false) {
150+
continue;
151+
}
152+
153+
$this->output->section(\sprintf(
154+
'%s: %s (%.0f%% confidence)',
155+
\ucfirst((string) $result->analyzerName),
156+
$result->detectedType,
157+
$result->confidence * 100,
158+
));
159+
160+
if (!empty($result->suggestedTemplates)) {
161+
$this->output->writeln('Suggested templates:');
162+
foreach ($result->suggestedTemplates as $templateName) {
163+
$template = $templateRegistry->getTemplate($templateName);
164+
if ($template !== null) {
165+
$this->output->writeln(\sprintf(' - %s: %s', $templateName, $template->description));
166+
}
167+
}
168+
}
169+
}
170+
171+
// Use the best match (could be fallback if no other matches)
172+
$bestResult = $results[0];
173+
$primaryTemplate = $bestResult->getPrimaryTemplate();
174+
175+
if ($primaryTemplate !== null) {
176+
$template = $templateRegistry->getTemplate($primaryTemplate);
177+
if ($template !== null) {
178+
$this->output->note(\sprintf('Using template: %s', $template->description));
179+
return $this->writeConfig($files, $template->config, $type, $filePath);
180+
}
181+
}
182+
183+
// This should never happen, but provide safety fallback
184+
$this->output->error('No suitable template found');
185+
return Command::FAILURE;
186+
}
187+
188+
private function writeConfig(
189+
FilesInterface $files,
190+
ConfigRegistry $config,
191+
ConfigType $type,
192+
string $filePath,
193+
): int {
73194
try {
74195
$content = match ($type) {
75196
ConfigType::Json => \json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
@@ -85,13 +206,6 @@ public function __invoke(DirectoriesInterface $dirs, FilesInterface $files): int
85206
};
86207
} catch (\Throwable $e) {
87208
$this->output->error(\sprintf('Failed to create config: %s', $e->getMessage()));
88-
89-
return Command::FAILURE;
90-
}
91-
92-
if ($files->exists($filePath)) {
93-
$this->output->error(\sprintf('Config %s already exists', $filePath));
94-
95209
return Command::FAILURE;
96210
}
97211

src/Console/context.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,13 @@ documents:
1515
filePattern: '*.php'
1616
showTreeView: true
1717

18+
- description: Init command
19+
outputPath: console/init.md
20+
sources:
21+
- type: file
22+
sourcePaths:
23+
- ./InitCommand.php
24+
- ../DirectoriesInterface.php
25+
- ../Application/FSPath.php
26+
- ../Application/Bootloader/ConsoleBootloader.php
27+

0 commit comments

Comments
 (0)