Skip to content

Commit 07deb8c

Browse files
committed
feat: add project management commands for dynamic context switching
Implements new console commands that allow switching between projects. This feature enables seamless work across multiple codebases while using CTX with Claude. Key features: - Switch to any project with `ctx project` or `ctx project .` (current dir) - Use project aliases for quick switching with `ctx project <alias>` - Add multiple projects with `ctx project:add <path>` - View registered projects with `ctx project:list` - Store project state based on OS conventions
1 parent 9e60ec0 commit 07deb8c

17 files changed

+1060
-101
lines changed

app.php

+8
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,19 @@
9292
// Execute Application
9393
// -----------------------------------------------------------------------------
9494

95+
// Determine appropriate location for global state based on OS
96+
$globalStateDir = match (PHP_OS_FAMILY) {
97+
'Windows' => \getenv('APPDATA') . '/CTX',
98+
'Darwin' => $_SERVER['HOME'] . '/Library/Application Support/CTX',
99+
default => $_SERVER['HOME'] . '/.config/ctx',
100+
};
101+
95102
$app = Kernel::create(
96103
directories: [
97104
'root' => $appPath,
98105
'output' => $appPath . '/.context',
99106
'config' => $appPath,
107+
'global-state' => $globalStateDir,
100108
'json-schema' => __DIR__,
101109
],
102110
exceptionHandler: ExceptionHandler::class,

src/Application/Kernel.php

-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use Butschster\ContextGenerator\Application\Bootloader\SourceFetcherBootloader;
1919
use Butschster\ContextGenerator\Application\Bootloader\VariableBootloader;
2020
use Butschster\ContextGenerator\McpServer\McpServerBootloader;
21-
use Butschster\ContextGenerator\McpServer\Prompt\McpPromptBootloader;
2221
use Butschster\ContextGenerator\Modifier\PhpContentFilter\PhpContentFilterBootloader;
2322
use Butschster\ContextGenerator\Modifier\PhpDocs\PhpDocsModifierBootloader;
2423
use Butschster\ContextGenerator\Modifier\PhpSignature\PhpSignatureModifierBootloader;
@@ -83,7 +82,6 @@ protected function defineBootloaders(): array
8382

8483
// MCP Server
8584
McpServerBootloader::class,
86-
McpPromptBootloader::class,
8785
];
8886
}
8987

src/Config/Import/Merger/VariablesConfigMerger.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
#[LoggerPrefix(prefix: 'variables-merger')]
1111
final readonly class VariablesConfigMerger extends AbstractConfigMerger
1212
{
13-
protected function performMerge(array $mainSection, array $importedSection, ImportedConfig $importedConfig): array
13+
public function getConfigKey(): string
1414
{
15-
// Merge the variables from the imported config into the main config
16-
return [...$importedSection, ...$mainSection];
15+
return 'variables';
1716
}
1817

19-
public function getConfigKey(): string
18+
protected function performMerge(array $mainSection, array $importedSection, ImportedConfig $importedConfig): array
2019
{
21-
return 'variables';
20+
// Merge the variables from the imported config into the main config
21+
return [...$importedSection, ...$mainSection];
2222
}
2323
}

src/Config/Loader/ConfigLoaderFactory.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function __construct(
3030
public function create(string $configPath): ConfigLoaderInterface
3131
{
3232
$dirs = $this->dirs->withConfigPath($configPath);
33-
$configPathObj = $dirs->getConfigPath();
33+
$configPathObj = $dirs->getRootPath();
3434

3535
// Create composite parser using the injected plugin registry
3636
$compositeParser = new CompositeConfigParser(

src/Console/Renderer/Style.php

+33-90
Original file line numberDiff line numberDiff line change
@@ -5,153 +5,96 @@
55
namespace Butschster\ContextGenerator\Console\Renderer;
66

77
/**
8-
* @deprecated Should be completely redesigned
8+
* Console output styling utilities
99
*/
1010
final class Style
1111
{
1212
/**
13-
* Format a section header (large titles)
13+
* Create a header styled text
1414
*/
1515
public static function header(string $text): string
1616
{
17-
return "\033[1;33m" . $text . "\033[0m";
17+
return \sprintf('<fg=bright-blue;options=bold>%s</>', $text);
1818
}
1919

2020
/**
21-
* Format a title (section names)
22-
*/
23-
public static function title(string $text): string
24-
{
25-
return "\033[1;36m" . $text . "\033[0m";
26-
}
27-
28-
/**
29-
* Format a subtitle (smaller section names)
21+
* Create a separator line
3022
*/
31-
public static function subtitle(string $text): string
23+
public static function separator(string $char = '-', int $length = 80): string
3224
{
33-
return "\033[1;34m" . $text . "\033[0m";
25+
return \sprintf('<fg=blue>%s</>', \str_repeat($char, $length));
3426
}
3527

3628
/**
37-
* Format a property name
29+
* Create a property name styled text
3830
*/
3931
public static function property(string $text): string
4032
{
41-
return "\033[1;32m" . $text . "\033[0m";
42-
}
43-
44-
/**
45-
* Format a value
46-
*/
47-
public static function value(mixed $value): string
48-
{
49-
if (\is_bool($value)) {
50-
return $value ? "\033[0;32mtrue\033[0m" : "\033[0;31mfalse\033[0m";
51-
}
52-
53-
if (\is_string($value)) {
54-
return "\033[0;33m\"" . $value . "\"\033[0m";
55-
}
56-
57-
if (\is_null($value)) {
58-
return "\033[0;90mnull\033[0m";
59-
}
60-
61-
if (\is_numeric($value)) {
62-
return "\033[0;36m" . (string) $value . "\033[0m";
63-
}
64-
65-
return (string) $value;
33+
return \sprintf('<fg=bright-cyan>%s</>', $text);
6634
}
6735

6836
/**
69-
* Format array values
37+
* Create a label styled text
7038
*/
71-
public static function array(array $values): string
39+
public static function label(string $text): string
7240
{
73-
if (empty($values)) {
74-
return "\033[0;90m[]\033[0m";
75-
}
76-
77-
if (\array_keys($values) === \range(0, \count($values) - 1)) {
78-
// Sequential array
79-
$formattedItems = \array_map(
80-
static fn($item) => \is_array($item) ? self::array($item) : self::value($item),
81-
$values,
82-
);
83-
return "\033[0;33m[" . \implode(", ", $formattedItems) . "]\033[0m";
84-
}
85-
86-
// Associative array
87-
$result = "{\n";
88-
foreach ($values as $key => $value) {
89-
$formattedValue = \is_array($value) ? self::array($value) : self::value($value);
90-
$result .= self::indent(self::property('"' . $key . '"') . ": " . $formattedValue) . "\n";
91-
}
92-
$result .= '}';
93-
return $result;
41+
return \sprintf('<fg=yellow>%s</>', $text);
9442
}
9543

9644
/**
97-
* Format a key-value pair
45+
* Create a count styled text
9846
*/
99-
public static function keyValue(string $key, mixed $value): string
47+
public static function count(int $count): string
10048
{
101-
$formattedValue = \is_array($value) ? self::array($value) : self::value($value);
102-
return self::property($key) . ": " . $formattedValue;
49+
return \sprintf('<fg=bright-magenta>%d</>', $count);
10350
}
10451

10552
/**
106-
* Format a count indicator
53+
* Create an item number styled text
10754
*/
108-
public static function count(int $count): string
55+
public static function itemNumber(int $current, int $total): string
10956
{
110-
return "\033[1;35m" . $count . "\033[0m";
57+
return \sprintf('<fg=bright-yellow>%d/%d</>', $current, $total);
11158
}
11259

11360
/**
114-
* Format an item number in a list
61+
* Create indented text
11562
*/
116-
public static function itemNumber(int $number, int $total = 10): string
63+
public static function indent(string $text, int $spaces = 2): string
11764
{
118-
return "\033[1;35m" . $number . '/' . $total . "\033[0m";
65+
$indent = \str_repeat(' ', $spaces);
66+
return $indent . \str_replace("\n", "\n" . $indent, $text);
11967
}
12068

12169
/**
122-
* Format path text
70+
* Create success styled text
12371
*/
124-
public static function path(string $path): string
72+
public static function success(string $text): string
12573
{
126-
return "\033[0;36m" . $path . "\033[0m";
74+
return \sprintf('<fg=green>%s</>', $text);
12775
}
12876

12977
/**
130-
* Format a preview of content
78+
* Create error styled text
13179
*/
132-
public static function contentPreview(string $content): string
80+
public static function error(string $text): string
13381
{
134-
$preview = \substr($content, 0, 100);
135-
if (\strlen($content) > 100) {
136-
$preview .= '...';
137-
}
138-
return "\033[0;90m\"" . \str_replace(["\r", "\n"], ["\\r", "\\n"], $preview) . "\"\033[0m";
82+
return \sprintf('<fg=red>%s</>', $text);
13983
}
14084

14185
/**
142-
* Indent a text block
86+
* Create warning styled text
14387
*/
144-
public static function indent(string $text, int $level = 1): string
88+
public static function warning(string $text): string
14589
{
146-
$indent = \str_repeat(' ', $level);
147-
return $indent . \str_replace("\n", "\n" . $indent, $text);
90+
return \sprintf('<fg=yellow>%s</>', $text);
14891
}
14992

15093
/**
151-
* Create a separator line
94+
* Create highlighted text
15295
*/
153-
public static function separator(string $char = '-', int $length = 30): string
96+
public static function highlight(string $text): string
15497
{
155-
return "\033[0;90m" . \str_repeat($char, $length) . "\033[0m";
98+
return \sprintf('<fg=bright-green>%s</>', $text);
15699
}
157100
}

src/McpServer/Console/MCPServerCommand.php

+20-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Butschster\ContextGenerator\Config\Loader\ConfigLoaderInterface;
1414
use Butschster\ContextGenerator\Console\BaseCommand;
1515
use Butschster\ContextGenerator\DirectoriesInterface;
16+
use Butschster\ContextGenerator\McpServer\Projects\ProjectService;
1617
use Butschster\ContextGenerator\McpServer\ProjectService\ProjectServiceFactory;
1718
use Butschster\ContextGenerator\McpServer\ProjectService\ProjectServiceInterface;
1819
use Butschster\ContextGenerator\McpServer\ServerRunnerInterface;
@@ -45,8 +46,25 @@ final class MCPServerCommand extends BaseCommand
4546
)]
4647
protected ?string $envFileName = null;
4748

48-
public function __invoke(Container $container, DirectoriesInterface $dirs, Application $app): int
49-
{
49+
public function __invoke(
50+
Container $container,
51+
DirectoriesInterface $dirs,
52+
Application $app,
53+
ProjectService $projects,
54+
): int {
55+
$currentProject = $projects->getCurrentProject();
56+
if ($this->configPath === null && $currentProject) {
57+
$this->configPath = $currentProject->hasConfigFile()
58+
? $currentProject->getConfigFile()
59+
: $currentProject->path;
60+
61+
if ($this->envFileName === null) {
62+
$this->envFileName = $currentProject->hasEnvFile()
63+
? $currentProject->getEnvFile()
64+
: null;
65+
}
66+
}
67+
5068
// Determine the effective root path based on config file path
5169
$dirs = $dirs
5270
->determineRootPath($this->configPath)

src/McpServer/McpServerBootloader.php

+5
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
use Butschster\ContextGenerator\McpServer\Action\Tools\Prompts\GetPromptToolAction;
2929
use Butschster\ContextGenerator\McpServer\Action\Tools\Prompts\ListPromptsToolAction;
3030
use Butschster\ContextGenerator\McpServer\Console\MCPServerCommand;
31+
use Butschster\ContextGenerator\McpServer\Projects\McpProjectsBootloader;
3132
use Butschster\ContextGenerator\McpServer\ProjectService\ProjectServiceInterface;
33+
use Butschster\ContextGenerator\McpServer\Prompt\McpPromptBootloader;
3234
use Butschster\ContextGenerator\McpServer\Registry\McpItemsRegistry;
3335
use Butschster\ContextGenerator\McpServer\Routing\McpResponseStrategy;
3436
use Butschster\ContextGenerator\McpServer\Routing\RouteRegistrar;
@@ -55,6 +57,9 @@ public function defineDependencies(): array
5557
return [
5658
HttpClientBootloader::class,
5759
McpToolBootloader::class,
60+
McpPromptBootloader::class,
61+
McpProjectsBootloader::class,
62+
5863
];
5964
}
6065

0 commit comments

Comments
 (0)