Skip to content

Commit 6408f57

Browse files
authored
Merge pull request #1 from stellarwp/feature/add-cli-package
Feature: add cli package
2 parents 2417397 + aec4623 commit 6408f57

57 files changed

Lines changed: 2417 additions & 8 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,122 @@ Initial packages:
1010
- `stellarwp/foundation-log`
1111
- `stellarwp/foundation-pipeline`
1212

13+
## Namespaces
14+
1315
Use package namespaces under `StellarWP\Foundation\<Package>\`.
1416

15-
TODO: finish this.
17+
## Code Organization
18+
19+
Prefer feature-first organization, also known as vertical slice architecture or package-by-feature, when adding command/tooling features. Group a command and its private collaborators under the command feature namespace.
20+
21+
For example, use:
22+
23+
```text
24+
Commands/
25+
Package/
26+
Contracts/
27+
PackageRepositoryCreator.php
28+
CreateCommand.php
29+
PackageResolver.php
30+
PackageFilesValidator.php
31+
GitHubPackageRepositoryCreator.php
32+
```
33+
34+
instead of splitting those private collaborators into broad technical folders too early.
35+
36+
If a collaborator is only useful for one command group, keep it under that command group's feature folder. If it becomes useful across multiple command groups, promote it to a broader domain or infrastructure namespace such as `Package/`, `GitHub/`, `Console/`, or `Process/`.
37+
38+
When it is very clear that a class will be reused by many similar features, promote it immediately instead of burying it in the first feature slice. This is especially true for command/tooling infrastructure where many commands will need the same capability, such as shell command formatting, process execution, console IO helpers, package discovery, or GitHub API clients.
39+
40+
Feature-local interfaces should live in a `Contracts/` folder inside the feature slice, for example `Commands/Package/Contracts/PackageRepositoryCreator.php`. Only promote contracts to a package-level `Contracts/` namespace when they are intended to be shared across multiple features or consumed as public extension points.
41+
42+
Shared infrastructure interfaces should live under that shared namespace's `Contracts/` folder, for example `Process/Contracts/ProcessRunner.php`.
43+
44+
## Container Providers
45+
46+
When writing providers or container registration code, prefer container-driven construction over inline factories with explicit `new` calls. Bind classes and interfaces directly when the container can autowire them.
47+
48+
Use contextual bindings with `$this->container->when()->needs()->give()` for scalar constructor arguments, command lists, or feature-specific substitutions. Use a factory closure only when the value must be computed or resolved from the container, and keep that closure focused on supplying the constructor dependency rather than constructing the full object.
49+
50+
## Split Packages
51+
52+
Split packages live in `src/<Package>/` and are split to read-only repositories named `stellarwp/foundation-<package>`.
53+
54+
When adding a new split package, set its package `composer.json` PHP constraint to `>=8.3` unless the user explicitly says otherwise. PHP 7.4 release compatibility will be handled later by an automated Rector downgrade workflow, not by lowering the package PHP constraint during development.
55+
56+
When adding external dependencies for split packages, choose version constraints whose package line supports PHP 7.4. Use `>=` constraints for those dependencies instead of caret constraints when preserving the PHP 7.4-compatible floor matters. For example, use a Symfony component version such as `>=5.4` rather than a newer line that requires PHP 8+.
57+
58+
Important exception: dependencies on this monorepo's own split packages, such as `stellarwp/foundation-container`, should use the correct Composer release constraint like `^1.0`. Do not use `>=` for internal Foundation package dependencies; Monorepo Builder commands such as `composer monorepo bump-interdependency` are expected to bump those constraints during releases.
59+
60+
### Required Files
61+
62+
Each split package should include:
63+
64+
- `composer.json`
65+
- `README.md`
66+
- `.gitattributes`
67+
- `.gitignore`
68+
- `.github/workflows/close-pull-request.yml`
69+
70+
Each split package `README.md` must include this warning immediately after the package heading:
71+
72+
```markdown
73+
> [!WARNING]
74+
> **This is a read-only repository!** For pull requests or issues, see [stellarwp/foundation](https://github.com/stellarwp/foundation).
75+
```
76+
77+
### GitHub Repositories
78+
79+
When creating a new split repository on GitHub, use the description `[READ ONLY] Subtree split of the Foundation <Component> component (see stellarwp/foundation)` and disable wikis, issues, projects, and pull requests.
80+
81+
## PHP Feature Policy
82+
83+
Allowed for current PHP 8.3 source:
84+
85+
- constructor property promotion
86+
- union types
87+
- intersection types
88+
- readonly properties/classes
89+
- enums
90+
- nullsafe operator
91+
- match expressions
92+
- named arguments
93+
- first-class callables
94+
- typed class constants
95+
96+
Avoid unless there is a clear reason:
97+
98+
- enums in public APIs
99+
- reflection-heavy code
100+
- attributes that affect runtime behavior
101+
- DNF types
102+
- `never` in public APIs
103+
104+
Banned while the project targets PHP 8.3:
105+
106+
- PHP 8.4 property hooks
107+
- PHP 8.4 asymmetric visibility
108+
- PHP 8.4 lazy objects API
109+
- `#[Deprecated]`; use `@deprecated` PHPDoc instead
110+
- PHP 8.4-only functions/classes/constants
111+
112+
## Monorepo Commands
113+
114+
After adding or changing split package dependencies, run `composer monorepo merge` and then `composer update` so root `composer.json`/lock state includes package dependency changes.
115+
116+
Use `composer monorepo list` to inspect available Monorepo Builder commands.
117+
118+
## Verification
119+
120+
When `composer lint` reports style-only issues, run `composer format` to let the project formatter fix them before making manual formatting edits.
121+
122+
Reusable test fixtures, sample classes, and test doubles should live under `tests/Support/Fixtures/<Namespace>/` instead of being declared inline in a test class file. Keep truly local one-off fakes inline only when they are not reusable and do not represent a domain/package fixture.
123+
124+
After completing a feature, run `composer test:coverage`, review `clover.xml` for missed source coverage, and add meaningful tests for uncovered behavior before considering the feature complete.
125+
126+
## Releases
127+
128+
- Adding a new split package is usually a minor SemVer release because it introduces new functionality without breaking existing packages. Use a major release only if the change also breaks an existing public API or package contract.
129+
- Run `composer monorepo bump-interdependency <version>` when planning a major version release so Foundation packages that depend on each other require the new major line. It may also be useful for a minor release when one package must require APIs added in that new minor.
130+
- Run `composer monorepo package-alias` when `dev-main` should move to a new development line, usually after a minor or major release. Do not run it for every patch release when the current branch alias is still correct.
131+
- The monorepo split workflow deploys package code to each sub-repository after a GitHub release is drafted.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

README.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,38 @@ After any needed command, commit the updated `composer.json` files. Then draft a
8080

8181
The [monorepo split GitHub workflow](./.github/workflows/monorepo-split.yml) will deploy each project's code to their sub-repository.
8282

83+
Adding a new split package is usually a minor [semver](https://semver.org/) release because it adds new functionality without breaking existing packages. Use a major release only if the change also breaks an existing public API or package contract.
84+
8385
### Monorepo
8486

8587
This uses [Symplify's Monorepo Builder](https://github.com/symplify/monorepo-builder). There is a shortcut composer script you can
8688
run to access their CLI: `composer monorepo list` to see the available commands.
8789

8890
#### Adding a New Package
8991

90-
1. Copy the composer.json from one of the packages and modify the `name` and `psr-4` autoload namespace.
91-
2. Ensure you have the `close-pull-request.yml` GitHub workflow, a `.gitattributes`, `.gitignore` and `README.md` files.
92-
3. Once you've added the specific dependencies your package needs to its composer.json, run `composer monorepo merge` and then `composer update` and commit the changes. This will merge the dependencies into the root composer.json.
93-
4. Create your [new repository](https://github.com/organizations/stellarwp/repositories/new), add the description: `[READ ONLY] Subtree split of the Foundation <NEW_COMPONENT_NAME> component (see stellarwp/foundation)` and disable wikis, issues, projects and pull requests.
92+
1. Run the package creation command:
93+
94+
```bash
95+
composer run foundation -- package:create <Package>
96+
```
97+
98+
The package argument can be a new package component such as `WPCli`, an existing package directory, short package name, or Composer package name, for example `Log`, `foundation-log`, or `stellarwp/foundation-log`.
99+
100+
If the package does not exist yet, the command asks whether to create the local scaffold in `src/<Package>` and asks for the Composer package name with a default such as `stellarwp/foundation-wpcli`. The scaffold includes the required `composer.json`, `README.md`, `.gitattributes`, `.gitignore`, and `close-pull-request.yml` files. After scaffolding, the command runs `composer monorepo merge` so the root `composer.json` includes the new package.
101+
102+
The command runs as a dry run by default. It validates the required split package files, prints the target repository name and description, and shows the GitHub CLI commands it will run.
103+
104+
2. Add source code, tests, and any package-specific dependencies to the new package.
105+
106+
3. Once you've added the specific dependencies your package needs to its composer.json, run `composer monorepo merge` again and then `composer update` and commit the changes. This will merge the dependency changes into the root composer.json.
107+
108+
4. Create and configure the read-only split repository:
109+
110+
```bash
111+
composer run foundation -- package:create <Package> --apply
112+
```
113+
114+
The command creates the `stellarwp/foundation-<package>` repository with the standard `[READ ONLY]` description, disables issues, wiki, and projects, and relies on the package's `close-pull-request.yml` workflow to close pull requests.
94115

95116
## License
96117

composer.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"monolog/monolog": "^2.11",
1818
"psr/log": ">=1.0",
1919
"stellarwp/container-contract": "^1.1",
20+
"symfony/console": ">=5.4",
2021
"vlucas/phpdotenv": ">=4.3"
2122
},
2223
"require-dev": {
@@ -26,20 +27,25 @@
2627
"phpstan/extension-installer": "^1.4",
2728
"phpstan/phpstan": "^2.2",
2829
"phpunit/phpunit": "^12.5",
30+
"wp-cli/wp-cli": ">=2.12",
2931
"zenphp/pinte": "^1.2"
3032
},
3133
"replace": {
34+
"stellarwp/foundation-cli": "self.version",
3235
"stellarwp/foundation-container": "self.version",
3336
"stellarwp/foundation-log": "self.version",
34-
"stellarwp/foundation-pipeline": "self.version"
37+
"stellarwp/foundation-pipeline": "self.version",
38+
"stellarwp/foundation-wpcli": "self.version"
3539
},
3640
"minimum-stability": "dev",
3741
"prefer-stable": true,
3842
"autoload": {
3943
"psr-4": {
44+
"StellarWP\\Foundation\\Cli\\": "src/Cli/",
4045
"StellarWP\\Foundation\\Container\\": "src/Container/",
4146
"StellarWP\\Foundation\\Log\\": "src/Log/",
42-
"StellarWP\\Foundation\\Pipeline\\": "src/Pipeline/"
47+
"StellarWP\\Foundation\\Pipeline\\": "src/Pipeline/",
48+
"StellarWP\\Foundation\\WPCli\\": "src/WPCli/"
4349
}
4450
},
4551
"autoload-dev": {
@@ -56,6 +62,7 @@
5662
}
5763
},
5864
"scripts": {
65+
"foundation": "@php src/Cli/bin/foundation",
5966
"monorepo": "@php vendor/bin/monorepo-builder",
6067
"test": "@php vendor/bin/phpunit -c phpunit.xml.dist --colors=always -d memory_limit=2G",
6168
"test:unit": "@test --testsuite Unit",

pinte.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{
22
"preset": "laravel",
33
"indent": "\t",
4+
"exclude": [
5+
".agents",
6+
".codex",
7+
".claude"
8+
],
49
"rules": {
510
"blank_line_after_opening_tag": false,
611
"linebreak_after_opening_tag": false,
@@ -13,6 +18,7 @@
1318
"combine_consecutive_unsets": true,
1419
"no_blank_lines_after_class_opening": false,
1520
"use_arrow_functions": true,
21+
"static_lambda": true,
1622
"blank_line_between_import_groups": true,
1723
"no_superfluous_phpdoc_tags": {
1824
"allow_mixed": true

src/Cli/.gitattributes

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Path-based git attributes
2+
# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
3+
4+
# Ignore paths when git creates an archive of this package
5+
.gitattributes export-ignore
6+
.gitignore export-ignore
7+
.github export-ignore
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: Close Pull Request
2+
3+
on:
4+
pull_request_target:
5+
types: [opened]
6+
7+
jobs:
8+
run:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: superbrothers/close-pull-request@v3
12+
with:
13+
comment: "This is a read-only repository. Please submit your PR on the https://github.com/stellarwp/foundation repository.<br><br>Thanks!"

src/Cli/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
vendor/
2+
composer.lock

src/Cli/Application.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace StellarWP\Foundation\Cli;
4+
5+
use StellarWP\Foundation\Cli\Contracts\CommandProvider;
6+
use Symfony\Component\Console\Application as SymfonyApplication;
7+
use Symfony\Component\Console\Command\Command;
8+
9+
/**
10+
* Symfony Console application for Foundation tooling.
11+
*
12+
* Use this as the CLI entry point when commands should be assembled by the
13+
* Foundation container, including commands contributed by command providers.
14+
*/
15+
final class Application extends SymfonyApplication
16+
{
17+
/**
18+
* @param iterable<Command> $commands
19+
* @param iterable<CommandProvider> $commandProviders
20+
*/
21+
public function __construct(iterable $commands = [], iterable $commandProviders = []) {
22+
parent::__construct('Foundation');
23+
24+
foreach ($commands as $command) {
25+
$this->addCommands([$command]);
26+
}
27+
28+
foreach ($commandProviders as $commandProvider) {
29+
$this->addCommandProvider($commandProvider);
30+
}
31+
}
32+
33+
public function addCommandProvider(CommandProvider $commandProvider): void {
34+
foreach ($commandProvider->commands() as $command) {
35+
$this->addCommands([$command]);
36+
}
37+
}
38+
}

src/Cli/CliProvider.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace StellarWP\Foundation\Cli;
4+
5+
use lucatume\DI52\Container;
6+
use StellarWP\Foundation\Cli\Commands\Package\Contracts\PackageRepositoryCreator;
7+
use StellarWP\Foundation\Cli\Commands\Package\CreateCommand;
8+
use StellarWP\Foundation\Cli\Commands\Package\GitHubPackageRepositoryCreator;
9+
use StellarWP\Foundation\Cli\Commands\Package\PackageFilesValidator;
10+
use StellarWP\Foundation\Cli\Commands\Package\PackageRepositoryPlanFactory;
11+
use StellarWP\Foundation\Cli\Commands\Package\PackageResolver;
12+
use StellarWP\Foundation\Cli\Commands\Package\PackageScaffolder;
13+
use StellarWP\Foundation\Cli\Process\Contracts\ProcessRunner;
14+
use StellarWP\Foundation\Cli\Process\ShellProcessRunner;
15+
use StellarWP\Foundation\Container\Contracts\Provider;
16+
17+
/**
18+
* Registers the default Foundation CLI application and command dependencies.
19+
*
20+
* Include this provider when booting the `foundation` executable so command
21+
* slices can be autowired through the Foundation container.
22+
*/
23+
final class CliProvider extends Provider
24+
{
25+
public const string ROOT_PATH = 'foundation.cli.root_path';
26+
27+
public function register(): void {
28+
$this->container->singleton(self::ROOT_PATH, getcwd() ?: dirname(__DIR__, 2));
29+
30+
$this->container->when(PackageResolver::class)
31+
->needs('$rootPath')
32+
->give(static fn (Container $c): string => $c->get(self::ROOT_PATH));
33+
34+
$this->container->when(PackageScaffolder::class)
35+
->needs('$rootPath')
36+
->give(static fn (Container $c): string => $c->get(self::ROOT_PATH));
37+
38+
$this->container->when(Application::class)
39+
->needs('$commands')
40+
->give(static fn (Container $c): array => [
41+
$c->get(CreateCommand::class),
42+
]);
43+
44+
$this->container->singleton(PackageResolver::class);
45+
$this->container->singleton(PackageScaffolder::class);
46+
$this->container->singleton(PackageFilesValidator::class);
47+
$this->container->singleton(PackageRepositoryPlanFactory::class);
48+
$this->container->singleton(ShellProcessRunner::class);
49+
$this->container->bind(ProcessRunner::class, ShellProcessRunner::class);
50+
$this->container->bind(PackageRepositoryCreator::class, GitHubPackageRepositoryCreator::class);
51+
$this->container->singleton(CreateCommand::class);
52+
$this->container->singleton(Application::class);
53+
}
54+
}

0 commit comments

Comments
 (0)