Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to specify a custom repository #12

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ cpx gives you multiple ways to run PHP code quickly, perfect for running scratch
- `cpx exec -r <raw php code>` will execute the given PHP code.
- `cpx tinker` will open an interactive REPL in the terminal for your project.

### cpx new

`cpx new <vendor>/<package> <project-name>` or `cpx new <vendor>/<package>:<version> <project-name>` will create a new project from the specified package. This is useful for quickly creating a new project without needing to install the package globally.

When using these commands, you get the following benefits:

- **Automatic Autoloaders** - When running a PHP file, it will automatically detect and use Composer's autoloader if it exists in the current or a parent directory
Expand All @@ -96,6 +100,23 @@ When using these commands, you get the following benefits:

`cpx help` will show a list of all the commands available in cpx.

## Advanced Usage:

### Specifying a custom repository

If you want to use a custom repository to install packages from, you can specify it using the `--repo` flag:

1. composer registries: `cpx <vend>/<pack> --repo=https://composer.example.com`
- supported schemes: `http:`, `https:`
2. git repos: `cpx <vend>/<pack> --repo=git+https://github.com/<vendor>/<repo>`
- supported schemes: `git+http:`, `git+https:`, `ssh:`, `git+ssh:`
3. local paths: `cpx <vend>/<pack> --repo=path:/some/place/on/disk`
- supported schemes: `file:`, `path:`

This also extends to the new command:

`cpx new <vend>/<pack> project-name --repo=git+https://gitlab.com/<vendor>/<repo>`

## FAQ:

### Why not just use global composer?
Expand Down Expand Up @@ -124,5 +145,10 @@ Yes, cpx will manage the package versions for you, so you can run any version of
The code is deliberately written in a way that it doesn't need any dependencies to run, so it has no chance of conflicting with your global composer dependencies if you use them for other things, as this is one of the problems cpx is trying to solve.

## Credits

- [Liam Hammett](https://github.com/imliam)
- [All Contributors](https://github.com/imliam/cpx/contributors)

```

```
4 changes: 3 additions & 1 deletion cpx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use Cpx\Commands\FormatCommand;
use Cpx\Commands\TinkerCommand;
use Cpx\Commands\UpdateCommand;
use Cpx\Commands\AliasesCommand;
use Cpx\Commands\NewCommand;
use Cpx\Commands\UpgradeCommand;
use Cpx\Commands\VersionCommand;

Expand Down Expand Up @@ -55,9 +56,10 @@ $command = match (true) {
$console->command === 'test' => TestCommand::class,
$console->command === 'tinker' => TinkerCommand::class,
$console->command === 'version' => VersionCommand::class,
$console->command === 'new' => NewCommand::class,
file_exists(realpath($console->command)) && !is_dir(realpath($console->command)) => (new ExecCommand(Console::parse("exec {$console->command} {$console->getCommandInput()}")))(),
array_key_exists($console->command, PackageAliases::$packages) => Package::parse(PackageAliases::$packages[$console->command]['package'])->runCommand($console),
str_contains($console->command, '/') => Package::parse($console->command)->runCommand($console),
str_contains($console->command, '/') => Package::parse($console->command, $console->options)->runCommand($console),
$console->command === '--version' || $console->command === '-v' || $console->hasOption('version') || $console->hasOption('v') => VersionCommand::class,
default => (new HelpCommand($console))(true),
};
Expand Down
29 changes: 15 additions & 14 deletions src/Commands/HelpCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@ public function __invoke(bool $unknownCommand = false)

$this->success('cpx - A Composer package runner with on-demand execution and package management.');
$this->line('Usage:');
$this->line(' ' . Command::COLOR_GREEN . 'cpx <vendor/package[:version]> [args] ' . Command::COLOR_RESET . 'Run a Composer package\'s bin command');
$this->line(' ' . Command::COLOR_GREEN . 'cpx check ' . Command::COLOR_RESET . 'Run a static analysis tool over a project');
$this->line(' ' . Command::COLOR_GREEN . 'cpx test ' . Command::COLOR_RESET . 'Run a testing framework over a project');
$this->line(' ' . Command::COLOR_GREEN . 'cpx format ' . Command::COLOR_RESET . 'Run a code formatter over a project');
$this->line(' ' . Command::COLOR_GREEN . 'cpx update ' . Command::COLOR_RESET . 'Update all packages');
$this->line(' ' . Command::COLOR_GREEN . 'cpx update <vendor/package> ' . Command::COLOR_RESET . 'Update all versions of a package');
$this->line(' ' . Command::COLOR_GREEN . 'cpx clean ' . Command::COLOR_RESET . 'Clean unused packages (older than 30 days)');
$this->line(' ' . Command::COLOR_GREEN . 'cpx clean --all ' . Command::COLOR_RESET . 'Clean all packages');
$this->line(' ' . Command::COLOR_GREEN . 'cpx exec </path/to/php/file.php> ' . Command::COLOR_RESET . 'Invoke a PHP file');
$this->line(' ' . Command::COLOR_GREEN . 'cpx exec -r <code> ' . Command::COLOR_RESET . 'Run PHP code without <?php ?> tags');
$this->line(' ' . Command::COLOR_GREEN . 'cpx tinker ' . Command::COLOR_RESET . 'Open an interactive REPL');
$this->line(' ' . Command::COLOR_GREEN . 'cpx list ' . Command::COLOR_RESET . 'List all installed packages');
$this->line(' ' . Command::COLOR_GREEN . 'cpx aliases ' . Command::COLOR_RESET . 'Show aliased package names to run via `cpx <alias>`');
$this->line(' ' . Command::COLOR_GREEN . 'cpx help ' . Command::COLOR_RESET . 'Show this help message');
$this->line(' ' . Command::COLOR_GREEN . 'cpx <vendor/package[:version]> [args] ' . Command::COLOR_RESET . 'Run a Composer package\'s bin command');
$this->line(' ' . Command::COLOR_GREEN . 'cpx new <vendor/package[:version]> <project-name> [args] ' . Command::COLOR_RESET . 'Create a new project from a Composer package');
$this->line(' ' . Command::COLOR_GREEN . 'cpx check ' . Command::COLOR_RESET . 'Run a static analysis tool over a project');
$this->line(' ' . Command::COLOR_GREEN . 'cpx test ' . Command::COLOR_RESET . 'Run a testing framework over a project');
$this->line(' ' . Command::COLOR_GREEN . 'cpx format ' . Command::COLOR_RESET . 'Run a code formatter over a project');
$this->line(' ' . Command::COLOR_GREEN . 'cpx update ' . Command::COLOR_RESET . 'Update all packages');
$this->line(' ' . Command::COLOR_GREEN . 'cpx update <vendor/package> ' . Command::COLOR_RESET . 'Update all versions of a package');
$this->line(' ' . Command::COLOR_GREEN . 'cpx clean ' . Command::COLOR_RESET . 'Clean unused packages (older than 30 days)');
$this->line(' ' . Command::COLOR_GREEN . 'cpx clean --all ' . Command::COLOR_RESET . 'Clean all packages');
$this->line(' ' . Command::COLOR_GREEN . 'cpx exec </path/to/php/file.php> ' . Command::COLOR_RESET . 'Invoke a PHP file');
$this->line(' ' . Command::COLOR_GREEN . 'cpx exec -r <code> ' . Command::COLOR_RESET . 'Run PHP code without <?php ?> tags');
$this->line(' ' . Command::COLOR_GREEN . 'cpx tinker ' . Command::COLOR_RESET . 'Open an interactive REPL');
$this->line(' ' . Command::COLOR_GREEN . 'cpx list ' . Command::COLOR_RESET . 'List all installed packages');
$this->line(' ' . Command::COLOR_GREEN . 'cpx aliases ' . Command::COLOR_RESET . 'Show aliased package names to run via `cpx <alias>`');
$this->line(' ' . Command::COLOR_GREEN . 'cpx help ' . Command::COLOR_RESET . 'Show this help message');
}
}
53 changes: 53 additions & 0 deletions src/Commands/NewCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Cpx\Commands;

use Cpx\Package;

class NewCommand extends Command
{
public function __invoke()
{
$console = $this->console;

if (count($console->arguments) < 1) {
throw new \Exception("No package name provided.");
}
if (count($console->arguments) < 2) {
throw new \Exception("No directory provided.");
}

$str = $console->arguments[0];
if (!str_contains($str, '/')) {
throw new \Exception("Invalid package name: {$str}");
}

$package = Package::parse($str, $console->options);

$installDir = $package->installOrUpdatePackage();

$config = [
"type" => "path",
"url" => "{$installDir}/vendor/{$package->vendor}/{$package->name}",
"options" => [
"symlink" => false
]
];

$projectDir = getcwd() . '/' . $console->arguments[1];

$command = "composer create-project $package->vendor/$package->name --stability=dev --repository='" . json_encode($config) . "' $projectDir";
foreach ($console->options as $key => $value) {
if ($key === 'repo') {
continue;
}
$command .= " --$key=$value";
}

$descriptorspec = [STDIN, STDOUT, STDOUT];
$proc = proc_open($command, $descriptorspec, $pipes);
proc_close($proc);
}
}
2 changes: 1 addition & 1 deletion src/Commands/UpdateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class UpdateCommand extends Command
public function __invoke()
{
match(true) {
str_contains($this->console->arguments[0] ?? '', '/') => $this->updatePackage(Package::parse($this->console->arguments[0])),
str_contains($this->console->arguments[0] ?? '', '/') => $this->updatePackage(Package::parse($this->console->arguments[0], $this->console->options)),
!empty($this->console->arguments[0]) => $this->updateVendor($this->console->arguments[0]),
default => $this->updateAllPackages(),
};
Expand Down
10 changes: 10 additions & 0 deletions src/Enums/RepositoryType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Cpx\Enums;

enum RepositoryType: string
{
case Composer = 'composer';
case Git = 'git';
case Path = 'path';
}
26 changes: 21 additions & 5 deletions src/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ protected function __construct(
public string $vendor,
public string $name,
public ?string $version = null,
public ?Repository $repository = null,
) {}

public static function parse(string $str): Package
public static function parse(string $str, $options = null): Package
{
if (empty($str)) {
throw new InvalidArgumentException('A package name must be provided.');
Expand All @@ -32,6 +33,11 @@ public static function parse(string $str): Package
$version = null;
}

if (is_array($options) && isset($options['repo'])) {
$repository = Repository::parse($options['repo']);
return new Package($vendor, $name, $version, $repository);
}

return new Package($vendor, $name, $version);
}

Expand Down Expand Up @@ -146,7 +152,10 @@ public function installOrUpdatePackage(bool $updateCheck = true): string
'config' => [
'allow-plugins' => true,
],
]));
], JSON_PRETTY_PRINT));

Repository::apply($this->repository, $installDir);

// Composer::runCommand("init --name=cpx-{$package->name} --version=1.0.0 --no-interaction", $installDir);

if ($this->version === null) {
Expand All @@ -156,9 +165,16 @@ public function installOrUpdatePackage(bool $updateCheck = true): string
}

Metadata::open()->updateLastCheckTime($this, 'updated')->save();
} elseif ($updateCheck && $this->shouldCheckForUpdates($this)) {

return $installDir;
}

$didChangeRepo = Repository::apply($this->repository, $installDir);

if ($updateCheck && ($didChangeRepo || $this->shouldCheckForUpdates($this))) {
printColor("Checking for updates for {$this}...");
$previousVersion = Composer::getCurrentVersion($installDir);
Repository::apply($this->repository, $installDir);
Composer::runCommand("update", $installDir);
$newVersion = Composer::getCurrentVersion($installDir);

Expand All @@ -169,10 +185,10 @@ public function installOrUpdatePackage(bool $updateCheck = true): string
}

Metadata::open()->updateLastCheckTime($this, 'updated')->save();
} else {
printColor("{$this} is already installed and doesn't need updating.");
return $installDir;
}

printColor("{$this} is already installed and doesn't need updating.");
return $installDir;
}

Expand Down
93 changes: 93 additions & 0 deletions src/Repository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace Cpx;

use Cpx\Enums\RepositoryType;

class Repository
{
protected function __construct(
public RepositoryType $type,
public ?string $url,
) {}

public static function parse(string $str): ?static
{

// Check for local path in the package name
$paths = ['file:', 'path:'];
foreach ($paths as $path) {
if (strpos($str, $path) === 0) {

$value = explode($path, $str)[1];
$value = str_replace('///', '/', $value);
$value = str_replace('//', '/', $value);

return new Repository(RepositoryType::Path, $value);
}
}

// Git
$gits = ['git+https://', 'git+http://', 'ssh://', 'git+ssh://'];
foreach ($gits as $git) {
if (strpos($str, $git) === 0) {

$value = str_replace('git+', '', $str);

return new Repository(RepositoryType::Git, $value);
}
}

$composers = ['https://', 'http://'];
foreach ($composers as $composer) {
if (strpos($str, $composer) === 0) {
return new Repository(RepositoryType::Composer, $str);
}
}

return null;
}

public function make(): ?array
{
switch ($this->type) {
case RepositoryType::Composer:
return [
'type' => 'composer',
'url' => $this->url,
];
case RepositoryType::Git:
return [
'type' => 'git',
'url' => $this->url,
];
case RepositoryType::Path:
return [
'type' => 'path',
'url' => $this->url,
];
}

return null;
}

public static function apply(?Repository $repo, string $installDir): bool
{
$original = file_get_contents("{$installDir}/composer.json");
$json = json_decode($original, true);

if (!isset($json['repositories']) || count($json['repositories'] ?? []) == 0) {
if ($repo === null) {
unset($json['repositories']);
} else {
$json['repositories'] = [$repo->make()];
}
} elseif (count($json['repositories']) > 0) {
$json['repositories'] = [$repo->make()];
}

$final = json_encode($json, JSON_PRETTY_PRINT);
file_put_contents("{$installDir}/composer.json", $final);
return $final !== $original;
}
}