Compare commits

...

24 Commits

Author SHA1 Message Date
jmiller 1690e291d2 Merge pull request 'chore(release): v07.00.00' (#119) from dev into main
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 1m18s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Access control (push) Successful in 4s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Scripts governance (push) Successful in 8s
Generic: Repo Health / Repository health (push) Successful in 18s
2026-05-26 02:55:34 +00:00
Jonathan Miller 7f818809ef chore(release): bump to 07.00.00
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Generic: Repo Health / Release configuration (push) Successful in 7s
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Successful in 6s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (push) Successful in 18s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Successful in 11s
Universal: Security Audit / Dependency Audit (pull_request) Successful in 9s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 1m23s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Successful in 1m14s
Generic: Repo Health / Release configuration (pull_request) Successful in 9s
Generic: Repo Health / Scripts governance (pull_request) Successful in 6s
Generic: Repo Health / Repository health (pull_request) Successful in 18s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Failing after 7s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Successful in 1m14s
Platform: moko-platform CI / Gate 4: Governance (pull_request) Successful in 1m11s
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Failing after 1m13s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Successful in 1m20s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Successful in 1m21s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 13s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Successful in 48s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Successful in 46s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 44s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Successful in 47s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 49s
Major release:
- 5 new CLI tools (client_provision, client_dashboard, client_health_check,
  joomla_compat_check, theme_lint)
- ConfigValidator for plugin JSON schema validation
- PHPUnit test infrastructure (19 tests)
- bin/moko plugin command dispatcher (45 commands)
- All CLIApp scripts migrated to CliFramework
- PHPStan level 2 with 0 errors, 0 exclusions
- bin/moko COMMAND_MAP fixed
- package_build.php Joomla package fix

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 21:54:46 -05:00
jmiller 597b40f3f2 Merge pull request 'chore: cascade main → dev (80108f9) [skip ci]' (#118) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 02:51:37 +00:00
jmiller 80108f9ca8 Merge pull request 'feat: ConfigValidator + plugin command dispatcher' (#117) from dev into main
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 4s
Generic: Repo Health / Release configuration (push) Successful in 4s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Repository health (push) Successful in 13s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 57s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 6s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Successful in 55s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Successful in 56s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 54s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 53s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Successful in 1m5s
2026-05-26 02:51:30 +00:00
Jonathan Miller b33623c731 feat: add ConfigValidator for plugin JSON schema validation
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Release configuration (push) Successful in 6s
Generic: Repo Health / Scripts governance (push) Successful in 6s
Generic: Repo Health / Repository health (push) Successful in 16s
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Validate PR (pull_request) Successful in 8s
Universal: PR Check / Build RC Package (pull_request) Successful in 3s
Generic: Repo Health / Release configuration (pull_request) Successful in 9s
Generic: Repo Health / Scripts governance (pull_request) Successful in 10s
Generic: Repo Health / Repository health (pull_request) Successful in 17s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 1m3s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Successful in 1m0s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 6s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Successful in 46s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Successful in 48s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 49s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 46s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Successful in 1m2s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Failing after 7s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Successful in 49s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Successful in 51s
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Failing after 53s
Platform: moko-platform CI / Gate 4: Governance (pull_request) Successful in 51s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Successful in 59s
Validates project config against plugin getConfigSchema() definitions.
Supports: type checking, required fields, enum values, nested objects,
arrays, string minLength/pattern, number min/max, unknown property warnings.

7 unit tests covering all validation paths.

Closes #105

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 21:50:37 -05:00
jmiller 9ff59ce405 Merge pull request 'chore: cascade main → dev (9c6f393) [skip ci]' (#116) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 02:48:49 +00:00
jmiller 9c6f393f92 Merge pull request 'feat: plugin command dispatcher + auto-grouped list' (#115) from dev into main
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 5s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 1m16s
Generic: Repo Health / Release configuration (push) Successful in 8s
Generic: Repo Health / Scripts governance (push) Successful in 7s
Generic: Repo Health / Repository health (push) Successful in 16s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 13s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Successful in 1m5s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Successful in 1m7s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 1m5s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Successful in 1m11s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 1m8s
2026-05-26 02:48:16 +00:00
Jonathan Miller a418798a4d feat: plugin command dispatcher + auto-grouped command list
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Successful in 4s
Generic: Repo Health / Scripts governance (push) Successful in 4s
Generic: Repo Health / Repository health (push) Successful in 15s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 1m1s
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: PR Check / Build RC Package (pull_request) Successful in 2s
Generic: Repo Health / Release configuration (pull_request) Successful in 5s
Generic: Repo Health / Scripts governance (pull_request) Successful in 4s
Generic: Repo Health / Repository health (pull_request) Successful in 17s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Successful in 1m12s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Failing after 4s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Successful in 47s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Successful in 50s
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Failing after 49s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Successful in 57s
Platform: moko-platform CI / Gate 4: Governance (pull_request) Successful in 55s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 5s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Successful in 50s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Successful in 52s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 49s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Successful in 56s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 51s
bin/moko list now auto-groups 45 commands into 12 categories with
color formatting. Also loads plugin commands dynamically via
getCommands() — when plugins define commands, they'll appear
automatically under "Plugin: <type>" groups.

Closes #104

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 21:46:58 -05:00
jmiller baafffb1be Merge pull request 'chore: cascade main → dev (44c6bcb) [skip ci]' (#113) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 02:39:06 +00:00
jmiller 1c930ca9bd Merge pull request 'feat: PHPUnit test infrastructure + 12 tests' (#114) from dev into main
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 5s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Successful in 4s
Generic: Repo Health / Scripts governance (push) Successful in 4s
Generic: Repo Health / Repository health (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 46s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 6s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Successful in 54s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 52s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 53s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Successful in 58s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Successful in 1m0s
2026-05-26 02:39:00 +00:00
Jonathan Miller 3e37035786 feat: set up PHPUnit test infrastructure with 12 tests
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Successful in 6s
Generic: Repo Health / Scripts governance (push) Successful in 6s
Generic: Repo Health / Repository health (push) Successful in 16s
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 4s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Generic: Repo Health / Release configuration (pull_request) Successful in 5s
Generic: Repo Health / Scripts governance (pull_request) Successful in 6s
Universal: PR Check / Build RC Package (pull_request) Successful in 2s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 59s
Generic: Repo Health / Repository health (pull_request) Successful in 15s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Successful in 50s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Successful in 50s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 6s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Successful in 48s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 51s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Successful in 59s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 58s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Failing after 6s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Successful in 1m0s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Successful in 1m6s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Successful in 1m5s
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Failing after 55s
Platform: moko-platform CI / Gate 4: Governance (pull_request) Successful in 54s
- phpunit.xml configuration
- VersionReadTest: 5 tests covering README, XML, suffix stripping,
  version comparison, and error cases
- VersionBumpTest: 7 tests covering patch/minor/major bumps, rollover,
  HTML comment format, suffixed XML, and error cases

All tests run CLI tools as subprocesses against temp fixtures.

Closes #102

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 21:38:02 -05:00
jmiller 5805358ef4 Merge pull request 'chore: cascade main → dev (44c6bcb) [skip ci]' (#112) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 02:29:41 +00:00
jmiller 44c6bcbc2d feat(cli): add client_health_check.php
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Generic: Repo Health / Release configuration (push) Successful in 6s
Generic: Repo Health / Scripts governance (push) Successful in 6s
Generic: Repo Health / Repository health (push) Successful in 13s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 55s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 7s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 47s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Failing after 51s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 51s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Failing after 54s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Failing after 57s
2026-05-26 02:29:37 +00:00
jmiller 78fcbdd4a9 feat(cli): add joomla_compat_check.php
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Scripts governance (push) Successful in 7s
Generic: Repo Health / Release configuration (push) Successful in 7s
Generic: Repo Health / Repository health (push) Successful in 14s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 54s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 5s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Failing after 43s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Failing after 49s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 50s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 50s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Failing after 56s
2026-05-26 02:29:36 +00:00
jmiller 4fd1acb68c feat(cli): add theme_lint.php
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Generic: Repo Health / Release configuration (push) Successful in 4s
Generic: Repo Health / Scripts governance (push) Successful in 4s
Generic: Repo Health / Repository health (push) Successful in 13s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 55s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 4s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Failing after 42s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 43s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Failing after 43s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 42s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Failing after 44s
2026-05-26 02:29:36 +00:00
jmiller 9f7599fdb1 Merge pull request 'chore: cascade main → dev (57a0b49) [skip ci]' (#111) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 02:25:24 +00:00
jmiller 57a0b491ea Merge pull request 'chore: update CLAUDE.md with current architecture' (#110) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Generic: Repo Health / Release configuration (push) Successful in 4s
Generic: Repo Health / Scripts governance (push) Successful in 4s
Generic: Repo Health / Repository health (push) Successful in 9s
2026-05-26 02:25:19 +00:00
Jonathan Miller f76cd94c64 chore: update CLAUDE.md with current architecture
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Scripts governance (push) Successful in 21s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Release configuration (push) Successful in 22s
Generic: Repo Health / Repository health (push) Successful in 23s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Generic: Repo Health / Release configuration (pull_request) Successful in 4s
Generic: Repo Health / Scripts governance (pull_request) Successful in 4s
Universal: PR Check / Build RC Package (pull_request) Successful in 1s
Generic: Repo Health / Repository health (pull_request) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Successful in 46s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Failing after 47s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Failing after 52s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Failing after 53s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Failing after 9s
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Failing after 51s
Platform: moko-platform CI / Gate 4: Governance (pull_request) Successful in 51s
- Language: HCL → PHP 8.1+
- Added directory layout table (cli/, validate/, lib/, etc.)
- Added CliFramework pattern for new tools
- Added code quality section (PHPCS, PHPStan levels)
- Added common commands (bin/moko, phpcs, phpstan)
- Added rules for new CLI tools and COMMAND_MAP registration

Closes #103

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 21:24:43 -05:00
jmiller ca1c3e0dba Merge pull request 'chore: cascade main → dev (9ee50d0) [skip ci]' (#109) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 02:21:14 +00:00
jmiller 9ee50d0058 Merge pull request 'chore: migrate 7 CLIApp scripts to CliFramework' (#108) from dev into main
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 4s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Repository health (push) Successful in 13s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 56s
2026-05-26 02:21:08 +00:00
Jonathan Miller bc67a53442 chore: migrate 7 CLIApp scripts to CliFramework + remove PHPStan excludes
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Repository health (push) Successful in 12s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Generic: Repo Health / Release configuration (pull_request) Successful in 5s
Generic: Repo Health / Scripts governance (pull_request) Successful in 5s
Universal: PR Check / Build RC Package (pull_request) Successful in 3s
Generic: Repo Health / Repository health (pull_request) Successful in 16s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 53s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Successful in 58s
All 7 legacy CLIApp scripts migrated to CliFramework:
- validate/check_wiki_health.php
- validate/auto_detect_platform.php
- cli/joomla_release.php
- automation/push_files.php
- automation/bulk_sync.php
- automation/bulk_joomla_template.php
- automation/repo_cleanup.php

Migration: extends CLIApp -> CliFramework, setupArguments() -> configure(),
addOption() -> addArgument(), getOption() -> getArgument(), boot code updated.

Also: removed PHPStan exclusions, fixed ApiClient::delete() signature,
renamed conflicting log()/error() overrides.

PHPStan level 2: 0 errors, 0 files excluded.

Closes #101

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 21:20:30 -05:00
jmiller 147cf663a6 Merge pull request 'chore: cascade main → dev (e41d9b9) [skip ci]' (#107) from main into dev
chore: cascade main → dev [skip ci]
2026-05-26 01:37:40 +00:00
jmiller e41d9b9335 Merge pull request 'fix(critical): bin/moko COMMAND_MAP paths + add all CLI tools' (#106) from dev into main
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Generic: Repo Health / Release configuration (push) Successful in 4s
Generic: Repo Health / Scripts governance (push) Successful in 4s
Generic: Repo Health / Repository health (push) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 46s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 4s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Failing after 37s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 37s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 38s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Failing after 39s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Failing after 42s
2026-05-26 01:37:36 +00:00
Jonathan Miller 5c5c5e9ff2 fix(critical): bin/moko COMMAND_MAP — remove api/ prefix, add all tools
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Successful in 3s
Generic: Repo Health / Scripts governance (push) Successful in 3s
Generic: Repo Health / Repository health (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 44s
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Release configuration (pull_request) Successful in 5s
Generic: Repo Health / Scripts governance (pull_request) Successful in 4s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Universal: PR Check / Validate PR (pull_request) Successful in 7s
Universal: PR Check / Build RC Package (pull_request) Successful in 1s
Generic: Repo Health / Repository health (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Successful in 49s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Failing after 4s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Failing after 40s
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Failing after 40s
Platform: moko-platform CI / Gate 4: Governance (pull_request) Successful in 40s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Failing after 41s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Failing after 41s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 4s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 39s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 40s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Failing after 40s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Failing after 40s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Failing after 42s
All paths pointed to non-existent api/ directory. Fixed to use actual
paths (automation/, validate/, cli/, maintenance/). Also added 20 missing
commands covering all CLI tools built this session.

Closes #100

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 20:36:23 -05:00
21 changed files with 1675 additions and 268 deletions
+18 -5
View File
@@ -18,15 +18,28 @@ Version format: `XX.YY.ZZ` (zero-padded semver).
## [Unreleased]
## [07.00.00] - 2026-05-25
### Added
- `cli/client_provision.php` — end-to-end client onboarding (addresses #4)
- `cli/client_dashboard.php` — unified client dashboard: health, SSL, uptime, releases (closes #3)
- `cli/client_provision.php` — end-to-end client onboarding from JSON config (closes #4)
- `cli/client_dashboard.php` — unified HTML dashboard: health, SSL, uptime, releases (closes #3)
- `cli/client_health_check.php`, `cli/joomla_compat_check.php`, `cli/theme_lint.php` — new CLI tools
- `lib/Enterprise/ConfigValidator.php` — JSON schema validator for plugin configs (closes #105)
- PHPUnit test infrastructure: `phpunit.xml` + 19 tests (closes #102)
- `bin/moko list` — auto-grouped command list with 45 commands, plugin command dispatcher (closes #104)
- `templates/client-provision-example.json` — example config for client provisioning
### Fixed
- `release_cascade.php`: accept `release-candidate` as stability value (was only accepting `rc`, causing cascade to silently skip)
- PHPStan bumped from level 0 to level 2 — fixed 67 type errors (undefined variables, missing methods, wrong signatures, dead code)
- `package_build.php`: fix 0-byte ZIP for Joomla package extensions — sub-zips now in `packages/` subdir, no double `pkg_pkg_` prefix, includes `language/` dir (closes #92)
- `bin/moko` COMMAND_MAP: all paths pointed to non-existent `api/` directory (closes #100)
- `release_cascade.php`: accept `release-candidate` as stability value (was silently skipping)
- `package_build.php`: fix 0-byte ZIP for Joomla packages — correct structure, no double prefix (closes #92)
- PHPStan: level 0 to 2, 67 type errors fixed, 0 exclusions
- `ApiClient::delete()`: accept optional body parameter for Gitea Contents API
### Changed
- Migrated all 7 CLIApp scripts to CliFramework (closes #101)
- Updated CLAUDE.md with current architecture, CLI patterns, code quality (closes #103)
- Wiki CLI_AUTOMATION page updated with all tools
## [06.00.00] - 2026-05-25
+74 -8
View File
@@ -4,34 +4,100 @@ This file provides guidance to Claude Code when working with this repository.
## Project Overview
**moko-platform** -- Enterprise automation, validation, sync, and governance engine for all Moko Consulting repositories
**moko-platform** Enterprise automation, validation, sync, and governance engine for all Moko Consulting repositories
| Field | Value |
|---|---|
| **Platform** | generic |
| **Language** | HCL |
| **Language** | PHP 8.1+ |
| **Default branch** | main |
| **License** | GPL-3.0-or-later |
| **Version** | 06.00.00 |
| **Wiki** | [moko-platform Wiki](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki) |
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
## Common Commands
```bash
composer install # Install PHP dependencies
composer install # Install PHP dependencies
php bin/moko health --path . # Run repo health check
php bin/moko check:syntax --path . # PHP syntax check
php bin/moko drift --org MokoConsulting # Scan for standards drift
php bin/moko dashboard --token $TOKEN -o dashboard.html # Generate client dashboard
# Code quality
php vendor/bin/phpcs --standard=phpcs.xml -n lib/ validate/ automation/ cli/
php vendor/bin/phpcbf --standard=phpcs.xml lib/ validate/ automation/ cli/
php vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=512M
# Run all checks
composer check
```
## Architecture
See the [wiki](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki) for architecture details.
### Directory Layout
| Directory | Purpose |
|-----------|---------|
| `cli/` | 32 standalone CLI tools (version, release, build, repo management) |
| `validate/` | 20 validation scripts (syntax, structure, manifests, drift) |
| `automation/` | 7 bulk operations (sync, push files, templates, cleanup) |
| `lib/Enterprise/` | Core library — CliFramework, ApiClient, adapters, validators |
| `lib/Enterprise/Plugins/` | 11 platform plugins (Joomla, Dolibarr, Node.js, Python, etc.) |
| `deploy/` | SFTP deployment scripts (Joomla, Dolibarr, health checks) |
| `definitions/` | Repository structure definitions (HCL format) |
| `templates/` | Workflow templates, config templates, docs templates |
| `.mokogitea/workflows/` | CI/CD workflows (Gitea Actions) |
| `bin/moko` | Unified CLI dispatcher — runs any tool via `php bin/moko <command>` |
### CLI Framework
All CLI tools extend `MokoEnterprise\CliFramework` (defined in `lib/Enterprise/CliFramework.php`).
Pattern for new tools:
```php
class MyTool extends CliFramework {
protected function configure(): void {
$this->setDescription('What this tool does');
$this->addArgument('--name', 'Description', 'default');
}
protected function run(): int {
$name = $this->getArgument('--name');
// ... business logic ...
return 0;
}
}
$app = new MyTool();
exit($app->execute());
```
Built-in flags: `--help`, `--verbose`, `--quiet`, `--dry-run`
### Platform Adapters
Git operations are abstracted via `GitPlatformAdapter` interface:
- `MokoGiteaAdapter` — for git.mokoconsulting.tech (primary)
- `GitHubAdapter` — for github.com mirrors
### Plugin System
Platform-specific logic lives in `lib/Enterprise/Plugins/`. Each plugin implements `ProjectPluginInterface` with methods for health checks, validation, build commands, and config schemas.
## Code Quality
| Tool | Level | Config |
|------|-------|--------|
| PHPCS | PSR-12 (errors only) | `phpcs.xml` |
| PHPStan | Level 2 | `phpstan.neon` |
PHPStan runs with `--memory-limit=512M` due to large codebase. CI enforces PHPCS errors; PHPStan is advisory (`continue-on-error`).
## Rules
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
- **Attribution**: use `Authored-by: Moko Consulting` in commits
- **Branch strategy**: develop on `dev`, merge to `main` for release
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates)
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
- **New CLI tools**: extend `CliFramework`, not `CLIApp` (legacy)
- **After adding a CLI tool**: register it in `bin/moko` COMMAND_MAP
+32 -38
View File
@@ -30,7 +30,7 @@ require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\{
AuditLogger,
CLIApp,
CliFramework,
Config,
GitPlatformAdapter,
MetricsCollector,
@@ -47,7 +47,7 @@ use MokoEnterprise\{
*
* Works with both GitHub and Gitea via the PlatformAdapterFactory.
*/
class BulkJoomlaTemplate extends CLIApp
class BulkJoomlaTemplate extends CliFramework
{
public const DEFAULT_ORG = 'MokoConsulting';
public const VERSION = '04.06.10';
@@ -56,22 +56,20 @@ class BulkJoomlaTemplate extends CLIApp
private AuditLogger $logger;
private Config $config;
protected function setupArguments(): array
protected function configure(): void
{
return [
'org:' => 'Organization (default: ' . self::DEFAULT_ORG . ')',
'scaffold' => 'Create a new Joomla template repository',
'sync' => 'Sync MokoStandards files to existing template repos',
'list' => 'List all joomla-template repositories',
'name:' => 'Template name for --scaffold (e.g. MokoTheme)',
'client:' => 'Joomla client: site (default) or administrator',
'repos:' => 'Target repositories for --sync (comma-separated, or use --all)',
'all' => 'Sync all repos tagged joomla-template',
'sync-updates' => 'Sync updates.xml between Gitea and GitHub for Joomla repos',
'private' => 'Create as private repository (--scaffold)',
'dry-run' => 'Preview changes without making them',
'yes' => 'Auto-confirm prompts',
];
$this->setDescription('Bulk Joomla template management');
$this->addArgument('--org', 'Organization', self::DEFAULT_ORG);
$this->addArgument('--scaffold', 'Create new template repo', false);
$this->addArgument('--sync', 'Sync files to template repos', false);
$this->addArgument('--list', 'List template repos', false);
$this->addArgument('--name', 'Template name for scaffold', '');
$this->addArgument('--client', 'Joomla client: site or admin', 'site');
$this->addArgument('--repos', 'Target repos (comma-separated)', '');
$this->addArgument('--all', 'Sync all tagged repos', false);
$this->addArgument('--sync-updates', 'Sync updates.xml', false);
$this->addArgument('--private', 'Create as private repo', false);
$this->addArgument('--yes', 'Auto-confirm', false);
}
protected function run(): int
@@ -88,23 +86,23 @@ class BulkJoomlaTemplate extends CLIApp
}
$this->logger = new AuditLogger('joomla_template');
$org = $this->getOption('org', self::DEFAULT_ORG);
$org = $this->getArgument('--org', self::DEFAULT_ORG);
$platform = $this->adapter->getPlatformName();
$this->log("Platform: {$platform} | Organization: {$org}", 'INFO');
if ($this->hasOption('list')) {
if ($this->getArgument('--list', false)) {
return $this->listTemplateRepos($org);
}
if ($this->hasOption('scaffold')) {
if ($this->getArgument('--scaffold', false)) {
return $this->scaffoldTemplate($org);
}
if ($this->hasOption('sync')) {
if ($this->getArgument('--sync', false)) {
return $this->syncTemplates($org);
}
if ($this->hasOption('sync-updates')) {
if ($this->getArgument('--sync-updates', false)) {
return $this->syncUpdatesBetweenPlatforms($org);
}
@@ -138,9 +136,9 @@ class BulkJoomlaTemplate extends CLIApp
private function scaffoldTemplate(string $org): int
{
$name = $this->getOption('name', '');
$client = $this->getOption('client', 'site');
$dryRun = $this->hasOption('dry-run');
$name = $this->getArgument('--name', '');
$client = $this->getArgument('--client', 'site');
$dryRun = $this->dryRun;
if (empty($name)) {
$this->log("❌ --name is required for --scaffold", 'ERROR');
@@ -176,7 +174,7 @@ class BulkJoomlaTemplate extends CLIApp
}
// Confirm
if (!$this->hasOption('yes')) {
if (!$this->getArgument('--yes', false)) {
echo "\nCreate repository {$org}/{$name}? [y/N]: ";
$handle = fopen('php://stdin', 'r');
$line = fgets($handle);
@@ -192,7 +190,7 @@ class BulkJoomlaTemplate extends CLIApp
// Create repository
$this->log("\nCreating repository...", 'INFO');
try {
$isPrivate = $this->hasOption('private');
$isPrivate = $this->getArgument('--private', false);
$this->adapter->createOrgRepo($org, $name, [
'description' => "Joomla {$client} template — {$name}",
'private' => $isPrivate,
@@ -263,10 +261,10 @@ class BulkJoomlaTemplate extends CLIApp
{
$repos = [];
if ($this->hasOption('all')) {
if ($this->getArgument('--all', false)) {
$repos = $this->findTemplateRepos($org);
} else {
$reposArg = $this->getOption('repos', '');
$reposArg = $this->getArgument('--repos', '');
if (empty($reposArg)) {
$this->log("❌ --repos or --all required for --sync", 'ERROR');
return 1;
@@ -284,7 +282,7 @@ class BulkJoomlaTemplate extends CLIApp
$this->log("\nSyncing " . count($repos) . " template repo(s)...", 'INFO');
$dryRun = $this->hasOption('dry-run');
$dryRun = $this->dryRun;
$success = 0;
$failed = 0;
@@ -741,7 +739,7 @@ class BulkJoomlaTemplate extends CLIApp
{
$repos = [];
if ($this->hasOption('all')) {
if ($this->getArgument('--all', false)) {
$repos = $this->findTemplateRepos($org);
// Also include waas-component repos
$allRepos = $this->adapter->listOrgRepos($org, true);
@@ -765,7 +763,7 @@ class BulkJoomlaTemplate extends CLIApp
return true;
});
} else {
$reposArg = $this->getOption('repos', '');
$reposArg = $this->getArgument('--repos', '');
if (empty($reposArg)) {
$this->log("❌ --repos or --all required for --sync-updates", 'ERROR');
return 1;
@@ -791,7 +789,7 @@ class BulkJoomlaTemplate extends CLIApp
$gitea = $adapters['gitea'];
$github = $adapters['github'];
$dryRun = $this->hasOption('dry-run');
$dryRun = $this->dryRun;
$this->log("\nSyncing updates.xml across Gitea <-> GitHub for " . count($repos) . " repo(s)...", 'INFO');
@@ -936,10 +934,6 @@ class BulkJoomlaTemplate extends CLIApp
// Execute if run directly
if (php_sapi_name() === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) {
$app = new BulkJoomlaTemplate(
'joomla-template',
'Bulk scaffold and sync Joomla template repositories',
BulkJoomlaTemplate::VERSION
);
$app = new BulkJoomlaTemplate();
exit($app->execute());
}
+23 -27
View File
@@ -26,7 +26,7 @@ use MokoEnterprise\{
AuditLogger,
CheckpointManager,
CircuitBreakerOpen,
CLIApp,
CliFramework,
Config,
GitPlatformAdapter,
MetricsCollector,
@@ -45,7 +45,7 @@ use MokoEnterprise\{
* Synchronizes MokoStandards files across multiple repositories using
* the Enterprise library for robust, audited operations.
*/
class BulkSync extends CLIApp
class BulkSync extends CliFramework
{
/**
* Default organization for bulk sync operations
@@ -65,6 +65,7 @@ class BulkSync extends CLIApp
private RepositorySynchronizer $synchronizer;
private AuditLogger $logger;
private CheckpointManager $checkpoints;
private MetricsCollector $metrics;
private SecurityValidator $security;
private PluginFactory $pluginFactory;
private ProjectTypeDetector $typeDetector;
@@ -76,21 +77,20 @@ class BulkSync extends CLIApp
/**
* Setup command-line arguments
*/
protected function setupArguments(): array
protected function configure(): void
{
return [
'org:' => 'GitHub organization (default: MokoConsulting)',
'repos:' => 'Specific repositories to sync (space-separated)',
'exclude:' => 'Repositories to exclude (space-separated)',
'skip-archived' => 'Skip archived repositories',
'yes' => 'Auto-confirm prompts',
'resume' => 'Resume from last checkpoint, skipping already-processed repositories',
'force' => 'Force overwrite of protected files (always_overwrite=false), except truly protected files',
'protect' => 'Apply/enforce main branch protection rules on all synced repositories',
'no-issue' => 'Skip creating a tracking issue in each target repository',
'update-branches' => 'After sync, merge main into all other open PR branches in each repo',
'health' => 'Run repo health checks after sync and include results in the report',
];
$this->setDescription('Bulk repository synchronization');
$this->addArgument('--org', 'Organization', self::DEFAULT_ORG);
$this->addArgument('--repos', 'Specific repos', '');
$this->addArgument('--exclude', 'Repos to exclude', '');
$this->addArgument('--skip-archived', 'Skip archived repos', false);
$this->addArgument('--yes', 'Auto-confirm', false);
$this->addArgument('--resume', 'Resume from checkpoint', false);
$this->addArgument('--force', 'Force overwrite', false);
$this->addArgument('--protect', 'Apply branch protection', false);
$this->addArgument('--no-issue', 'Skip tracking issue', false);
$this->addArgument('--update-branches', 'Merge main into branches', false);
$this->addArgument('--health', 'Run health checks', false);
}
/**
@@ -106,13 +106,13 @@ class BulkSync extends CLIApp
}
// Get configuration
$org = $this->getOption('org', self::DEFAULT_ORG);
$skipArchived = $this->hasOption('skip-archived');
$autoConfirm = $this->hasOption('yes');
$org = $this->getArgument('--org', self::DEFAULT_ORG);
$skipArchived = $this->getArgument('--skip-archived', false);
$autoConfirm = $this->getArgument('--yes', false);
// Get repository filters
$specificRepos = $this->parseRepositoryList($this->getOption('repos', ''));
$excludeRepos = $this->parseRepositoryList($this->getOption('exclude', ''));
$specificRepos = $this->parseRepositoryList($this->getArgument('--repos', ''));
$excludeRepos = $this->parseRepositoryList($this->getArgument('--exclude', ''));
$this->log("Organization: {$org}", 'INFO');
if (!empty($specificRepos)) {
@@ -139,7 +139,7 @@ class BulkSync extends CLIApp
// Load resume checkpoint if --resume is set
$alreadyProcessed = [];
if ($this->hasOption('resume')) {
if ($this->getArgument('--resume', false)) {
$checkpoint = $this->checkpoints->loadCheckpoint('bulk_sync');
if ($checkpoint !== null) {
$alreadyProcessed = array_keys($checkpoint['results']['repositories'] ?? []);
@@ -1424,10 +1424,6 @@ class BulkSync extends CLIApp
// Execute if run directly
if (php_sapi_name() === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) {
$app = new BulkSync(
'bulk-sync',
'Enterprise-grade bulk repository synchronization',
BulkSync::VERSION
);
$app = new BulkSync();
exit($app->execute());
}
+27 -29
View File
@@ -24,7 +24,7 @@ require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\{
ApiClient,
AuditLogger,
CLIApp,
CliFramework,
Config,
DefinitionParser,
GitPlatformAdapter,
@@ -51,7 +51,7 @@ use MokoEnterprise\{
* php push_files.php --files=".github/workflows/ci.yml,.github/workflows/codeql-analysis.yml" --repos=MokoCRM,WaasComponent
* php push_files.php --files=templates/foo.txt:docs/foo.txt --repos=MyRepo --direct
*/
class PushFiles extends CLIApp
class PushFiles extends CliFramework
{
public const DEFAULT_ORG = 'MokoConsulting';
public const VERSION = '04.06.00';
@@ -65,18 +65,17 @@ class PushFiles extends CLIApp
/**
* Setup command-line arguments
*/
protected function setupArguments(): array
protected function configure(): void
{
return [
'org:' => 'GitHub organization (default: ' . self::DEFAULT_ORG . ')',
'repos:' => 'Target repositories — comma or space-separated (required)',
'files:' => 'Files to push — destination paths or source:destination pairs, comma/space-separated (required)',
'message:' => 'Custom commit message (optional)',
'branch:' => 'Target branch for direct pushes (default: repo default branch). Ignored unless --direct is set',
'direct' => 'Push directly to target branch instead of creating a PR',
'yes' => 'Auto-confirm without prompting',
'no-issue' => 'Skip creating a tracking issue in each target repository',
];
$this->setDescription('Push files to remote repositories');
$this->addArgument('--org', 'GitHub organization', self::DEFAULT_ORG);
$this->addArgument('--repos', 'Target repos (comma-separated)', '');
$this->addArgument('--files', 'Files to push (comma-separated)', '');
$this->addArgument('--message', 'Custom commit message', '');
$this->addArgument('--branch', 'Target branch for direct pushes', '');
$this->addArgument('--direct', 'Push directly instead of PR', false);
$this->addArgument('--yes', 'Auto-confirm without prompting', false);
$this->addArgument('--no-issue', 'Skip creating tracking issue', false);
}
/**
@@ -90,11 +89,11 @@ class PushFiles extends CLIApp
return 1;
}
$org = $this->getOption('org', self::DEFAULT_ORG);
$reposArg = $this->getOption('repos', '');
$filesArg = $this->getOption('files', '');
$direct = $this->hasOption('direct');
$autoYes = $this->hasOption('yes');
$org = $this->getArgument('--org', self::DEFAULT_ORG);
$reposArg = $this->getArgument('--repos', '');
$filesArg = $this->getArgument('--files', '');
$direct = $this->getArgument('--direct', false);
$autoYes = $this->getArgument('--yes', false);
// Validate required arguments
if (empty($reposArg)) {
@@ -127,7 +126,7 @@ class PushFiles extends CLIApp
}
// Confirm before proceeding
if (!$autoYes && !$this->confirm($repoFileMaps, $direct)) {
if (!$autoYes && !$this->confirmPush($repoFileMaps, $direct)) {
$this->log('❌ Cancelled.', 'INFO');
return 0;
}
@@ -265,7 +264,8 @@ class PushFiles extends CLIApp
// Fall back to live detection
try {
$repoData = $this->api->get("/repos/{$org}/{$repo}");
return $this->typeDetector->detect($repoData, $org, $repo);
$result = $this->typeDetector->detect('.');
return $result['type'] ?? 'default';
} catch (\Exception $e) {
$this->log(" ⚠️ Could not detect platform for {$repo}, using 'default'", 'WARN');
return 'default';
@@ -277,7 +277,7 @@ class PushFiles extends CLIApp
*
* @param array<string, list<array{source: string, destination: string}>> $repoFileMaps
*/
private function confirm(array $repoFileMaps, bool $direct): bool
private function confirmPush(array $repoFileMaps, bool $direct): bool
{
if ($this->quiet) {
return true;
@@ -322,8 +322,8 @@ class PushFiles extends CLIApp
'repos' => [],
];
$customMessage = $this->getOption('message', '');
$targetBranch = $this->getOption('branch', '');
$customMessage = $this->getArgument('--message', '');
$targetBranch = $this->getArgument('--branch', '');
foreach ($repoFileMaps as $repo => $entries) {
$this->log("\n[{$repo}] Pushing " . count($entries) . ' file(s)...', 'INFO');
@@ -520,6 +520,7 @@ class PushFiles extends CLIApp
'direction' => 'desc',
]);
$existing = array_values($existing);
if (!empty($existing) && isset($existing[0]['number'])) {
$num = $existing[0]['number'];
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller']];
@@ -581,7 +582,7 @@ class PushFiles extends CLIApp
));
$repoList = implode("\n", array_map(fn($r) => "- `{$r}`", $failedRepos));
$fileArgs = $this->getOption('files', '');
$fileArgs = $this->getArgument('--files', '');
$title = "fix: push_files failed for {$failed} repo(s) — action required";
@@ -622,6 +623,7 @@ class PushFiles extends CLIApp
'direction' => 'desc',
]);
$existing = array_values($existing);
if (!empty($existing) && isset($existing[0]['number'])) {
$num = $existing[0]['number'];
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller']];
@@ -693,10 +695,6 @@ class PushFiles extends CLIApp
// Execute if run directly
if (php_sapi_name() === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) {
$app = new PushFiles(
'push-files',
'Push one or more specific files to one or more remote repositories',
PushFiles::VERSION
);
$app = new PushFiles();
exit($app->execute());
}
+72 -78
View File
@@ -21,7 +21,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\{ApiClient, AuditLogger, CLIApp, Config, GitPlatformAdapter, MetricsCollector, PlatformAdapterFactory};
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, GitPlatformAdapter, MetricsCollector, PlatformAdapterFactory};
/**
* Enterprise Repository Cleanup
@@ -36,7 +36,7 @@ use MokoEnterprise\{ApiClient, AuditLogger, CLIApp, Config, GitPlatformAdapter,
* 7. Verify and provision standard labels
* 8. Version drift detection
*/
class RepoCleanup extends CLIApp
class RepoCleanup extends CliFramework
{
private const VERSION = '04.06.00';
private const SYNC_PREFIX = 'chore/sync-mokostandards-';
@@ -60,40 +60,34 @@ class RepoCleanup extends CLIApp
private GitPlatformAdapter $adapter;
private AuditLogger $logger;
private MetricsCollector $metrics;
private bool $dryRun = false;
protected bool $dryRun = false;
private float $startTime;
protected function configure(): void
{
$this->setName('repo-cleanup');
$this->setDescription('Enterprise repository cleanup — branches, PRs, issues, workflows, labels, logs');
$this->setVersion(self::VERSION);
$this->addOption('org', 'GitHub organization', 'MokoConsulting');
$this->addOption('repos', 'Specific repositories (space-separated)', '');
$this->addOption('skip-archived', 'Skip archived repositories', false);
$this->addOption('close-issues', 'Close resolved tracking issues (merged PR = done)', false);
$this->addOption('lock-old-issues', 'Lock issues closed >30 days', false);
$this->addOption('clean-workflows', 'Delete cancelled/stale workflow runs', false);
$this->addOption('clean-logs', 'Delete workflow run logs older than --log-days', false);
$this->addOption('log-days', 'Days to keep logs (default: 30)', '30');
$this->addOption('delete-retired', 'Delete retired workflow files from repos', false);
$this->addOption('check-labels', 'Verify mokostandards label exists', false);
$this->addOption('check-drift', 'Check for version drift against README.md', false);
$this->addOption('all', 'Run all cleanup operations', false);
$this->addOption('yes', 'Auto-confirm prompts', false);
$this->addOption('dry-run', 'Preview changes without making them', false);
$this->addOption('verbose', 'Show detailed output', false);
$this->addOption('quiet', 'Suppress non-error output', false);
$this->addOption('json', 'Output results as JSON', false);
$this->setDescription('Enterprise repository cleanup');
$this->addArgument('--org', 'GitHub organization', 'MokoConsulting');
$this->addArgument('--repos', 'Specific repos (space-separated)', '');
$this->addArgument('--skip-archived', 'Skip archived repos', false);
$this->addArgument('--close-issues', 'Close resolved tracking issues', false);
$this->addArgument('--lock-old-issues', 'Lock issues closed >30 days', false);
$this->addArgument('--clean-workflows', 'Delete stale workflow runs', false);
$this->addArgument('--clean-logs', 'Delete old workflow logs', false);
$this->addArgument('--log-days', 'Days to keep logs', '30');
$this->addArgument('--delete-retired', 'Delete retired workflows', false);
$this->addArgument('--check-labels', 'Verify labels exist', false);
$this->addArgument('--check-drift', 'Check version drift', false);
$this->addArgument('--all', 'Run all operations', false);
$this->addArgument('--yes', 'Auto-confirm', false);
$this->addArgument('--json', 'Output as JSON', false);
}
protected function execute(): int
protected function run(): int
{
$this->startTime = microtime(true);
$org = $this->getOption('org', 'MokoConsulting');
$this->dryRun = (bool) $this->getOption('dry-run', false);
$runAll = (bool) $this->getOption('all', false);
$org = $this->getArgument('--org', 'MokoConsulting');
$this->dryRun = (bool) $this->getArgument('--dry-run', false);
$runAll = (bool) $this->getArgument('--all', false);
$config = Config::load();
@@ -101,24 +95,24 @@ class RepoCleanup extends CLIApp
$this->adapter = PlatformAdapterFactory::create($config);
$this->api = $this->adapter->getApiClient();
} catch (\Exception $e) {
$this->error('Failed to initialize platform adapter: ' . $e->getMessage());
$this->errorMsg('Failed to initialize platform adapter: ' . $e->getMessage());
return 1;
}
$this->logger = new AuditLogger('repo_cleanup');
$this->metrics = new MetricsCollector('repo_cleanup');
$this->log("🧹 MokoStandards Repository Cleanup v" . self::VERSION);
$this->log("Organization: {$org}");
$this->log("Current sync branch: " . self::CURRENT_BRANCH);
$this->logMsg("🧹 MokoStandards Repository Cleanup v" . self::VERSION);
$this->logMsg("Organization: {$org}");
$this->logMsg("Current sync branch: " . self::CURRENT_BRANCH);
if ($this->dryRun) {
$this->log("⚠️ DRY RUN — no changes will be made");
$this->logMsg("⚠️ DRY RUN — no changes will be made");
}
$this->log('');
$this->logMsg('');
$repos = $this->fetchRepositories($org);
$this->log("Found " . count($repos) . " repositories");
$this->log('');
$this->logMsg("Found " . count($repos) . " repositories");
$this->logMsg('');
$results = [
'repos_processed' => 0,
@@ -140,7 +134,7 @@ class RepoCleanup extends CLIApp
$name = $repo['name'];
$num = $i + 1;
$total = count($repos);
$this->log("[{$num}/{$total}] {$name}");
$this->logMsg("[{$num}/{$total}] {$name}");
$results['repos_processed']++;
try {
@@ -151,37 +145,37 @@ class RepoCleanup extends CLIApp
$cleaned = $this->cleanBranches($org, $name, $results) || $cleaned;
// Optional: close resolved issues
if ($runAll || $this->getOption('close-issues', false)) {
if ($runAll || $this->getArgument('--close-issues', false)) {
$cleaned = $this->closeResolvedIssues($org, $name, $results) || $cleaned;
}
// Optional: lock old closed issues
if ($runAll || $this->getOption('lock-old-issues', false)) {
if ($runAll || $this->getArgument('--lock-old-issues', false)) {
$cleaned = $this->lockOldIssues($org, $name, $results) || $cleaned;
}
// Optional: delete retired workflow files
if ($runAll || $this->getOption('delete-retired', false)) {
if ($runAll || $this->getArgument('--delete-retired', false)) {
$cleaned = $this->deleteRetiredWorkflows($org, $name, $results) || $cleaned;
}
// Optional: clean workflow runs
if ($runAll || $this->getOption('clean-workflows', false)) {
if ($runAll || $this->getArgument('--clean-workflows', false)) {
$cleaned = $this->cleanWorkflowRuns($org, $name, $results) || $cleaned;
}
// Optional: clean old logs
if ($runAll || $this->getOption('clean-logs', false)) {
if ($runAll || $this->getArgument('--clean-logs', false)) {
$cleaned = $this->cleanOldLogs($org, $name, $results) || $cleaned;
}
// Optional: check labels
if ($runAll || $this->getOption('check-labels', false)) {
if ($runAll || $this->getArgument('--check-labels', false)) {
$this->checkLabels($org, $name, $results);
}
// Optional: check version drift
if ($runAll || $this->getOption('check-drift', false)) {
if ($runAll || $this->getArgument('--check-drift', false)) {
$this->checkVersionDrift($org, $name, $results);
}
@@ -189,32 +183,32 @@ class RepoCleanup extends CLIApp
$results['repos_cleaned']++;
}
} catch (\Exception $e) {
$this->error("{$name}: " . $e->getMessage());
$this->errorMsg("{$name}: " . $e->getMessage());
$results['errors']++;
}
}
$duration = round(microtime(true) - $this->startTime, 1);
$this->log('');
$this->log('============================================================');
$this->log("🧹 Cleanup Complete ({$duration}s)");
$this->log('============================================================');
$this->log("Repos processed: {$results['repos_processed']}");
$this->log("Repos with changes: {$results['repos_cleaned']}");
$this->log("Branches deleted: {$results['branches_deleted']}");
$this->log("PRs closed: {$results['prs_closed']}");
$this->log("Issues closed: {$results['issues_closed']}");
$this->log("Issues locked: {$results['issues_locked']}");
$this->log("Retired files: {$results['retired_files']}");
$this->log("Workflow runs: {$results['runs_deleted']}");
$this->log("Logs cleaned: {$results['logs_deleted']}");
$this->log("Labels missing: {$results['labels_missing']}");
$this->log("Version drift: {$results['version_drift']}");
$this->log("Errors: {$results['errors']}");
$this->log('============================================================');
$this->logMsg('');
$this->logMsg('============================================================');
$this->logMsg("🧹 Cleanup Complete ({$duration}s)");
$this->logMsg('============================================================');
$this->logMsg("Repos processed: {$results['repos_processed']}");
$this->logMsg("Repos with changes: {$results['repos_cleaned']}");
$this->logMsg("Branches deleted: {$results['branches_deleted']}");
$this->logMsg("PRs closed: {$results['prs_closed']}");
$this->logMsg("Issues closed: {$results['issues_closed']}");
$this->logMsg("Issues locked: {$results['issues_locked']}");
$this->logMsg("Retired files: {$results['retired_files']}");
$this->logMsg("Workflow runs: {$results['runs_deleted']}");
$this->logMsg("Logs cleaned: {$results['logs_deleted']}");
$this->logMsg("Labels missing: {$results['labels_missing']}");
$this->logMsg("Version drift: {$results['version_drift']}");
$this->logMsg("Errors: {$results['errors']}");
$this->logMsg('============================================================');
if ($this->getOption('json', false)) {
if ($this->getArgument('--json', false)) {
$results['duration_seconds'] = $duration;
echo json_encode($results, JSON_PRETTY_PRINT) . "\n";
}
@@ -226,8 +220,8 @@ class RepoCleanup extends CLIApp
private function fetchRepositories(string $org): array
{
$specificRepos = trim((string) $this->getOption('repos', ''));
$skipArchived = (bool) $this->getOption('skip-archived', false);
$specificRepos = trim((string) $this->getArgument('--repos', ''));
$skipArchived = (bool) $this->getArgument('--skip-archived', false);
if (!empty($specificRepos)) {
$names = preg_split('/[\s,]+/', $specificRepos);
@@ -264,7 +258,7 @@ class RepoCleanup extends CLIApp
if (($pr['number'] ?? 0) > 0 && !$this->dryRun) {
$this->api->patch("/repos/{$org}/{$repo}/pulls/{$pr['number']}", ['state' => 'closed']);
}
$this->log(" 🔒 Closed PR #{$pr['number']} ({$name})");
$this->logMsg(" 🔒 Closed PR #{$pr['number']} ({$name})");
$results['prs_closed']++;
$changed = true;
}
@@ -279,7 +273,7 @@ class RepoCleanup extends CLIApp
continue;
}
}
$this->log(" 🗑️ Deleted branch: {$name}");
$this->logMsg(" 🗑️ Deleted branch: {$name}");
$results['branches_deleted']++;
$changed = true;
}
@@ -312,7 +306,7 @@ class RepoCleanup extends CLIApp
'state' => 'closed', 'state_reason' => 'completed',
]);
}
$this->log(" ✅ Closed issue #{$num} (PR #{$prNum} merged)");
$this->logMsg(" ✅ Closed issue #{$num} (PR #{$prNum} merged)");
$results['issues_closed']++;
$changed = true;
}
@@ -361,7 +355,7 @@ class RepoCleanup extends CLIApp
}
if ($results['issues_locked'] > 0) {
$this->log(" 🔒 Locked {$results['issues_locked']} old closed issue(s)");
$this->logMsg(" 🔒 Locked {$results['issues_locked']} old closed issue(s)");
}
return $changed;
}
@@ -396,7 +390,7 @@ class RepoCleanup extends CLIApp
'branch' => $defaultBranch,
]);
}
$this->log(" Deleted retired: {$wf} (from {$wfDir})");
$this->logMsg(" Deleted retired: {$wf} (from {$wfDir})");
$results['retired_files']++;
$changed = true;
} catch (\Exception $e) {
@@ -433,7 +427,7 @@ class RepoCleanup extends CLIApp
}
}
if ($results['runs_deleted'] > 0) {
$this->log(" 🔄 Cleaned {$results['runs_deleted']} workflow run(s)");
$this->logMsg(" 🔄 Cleaned {$results['runs_deleted']} workflow run(s)");
}
return $changed;
}
@@ -441,7 +435,7 @@ class RepoCleanup extends CLIApp
private function cleanOldLogs(string $org, string $repo, array &$results): bool
{
$changed = false;
$days = (int) $this->getOption('log-days', '30');
$days = (int) $this->getArgument('--log-days', '30');
$cutoff = date('Y-m-d\TH:i:s\Z', strtotime("-{$days} days"));
try {
@@ -465,7 +459,7 @@ class RepoCleanup extends CLIApp
}
if ($results['logs_deleted'] > 0) {
$this->log(" 📋 Cleaned {$results['logs_deleted']} old log(s)");
$this->logMsg(" 📋 Cleaned {$results['logs_deleted']} old log(s)");
}
return $changed;
}
@@ -475,7 +469,7 @@ class RepoCleanup extends CLIApp
try {
$this->api->get("/repos/{$org}/{$repo}/labels/mokostandards");
} catch (\Exception $e) {
$this->log(" ⚠️ Missing 'mokostandards' label");
$this->logMsg(" ⚠️ Missing 'mokostandards' label");
$results['labels_missing']++;
$this->api->resetCircuitBreaker();
}
@@ -495,7 +489,7 @@ class RepoCleanup extends CLIApp
$mokoContent = base64_decode($mokoFile['content'] ?? '');
if (preg_match('/standards_version:\s*(\d{2}\.\d{2}\.\d{2})/m', $mokoContent, $vm)) {
if ($vm[1] !== self::VERSION) {
$this->log(" ⚠️ Standards drift: {$vm[1]} (expected " . self::VERSION . ")");
$this->logMsg(" ⚠️ Standards drift: {$vm[1]} (expected " . self::VERSION . ")");
$results['version_drift']++;
}
}
@@ -510,14 +504,14 @@ class RepoCleanup extends CLIApp
// ─── Helpers ─────────────────────────────────────────────────────────
private function log(string $message): void
private function logMsg(string $message): void
{
if (!$this->getOption('quiet', false)) {
if (!$this->quiet) {
echo $message . "\n";
}
}
private function error(string $message): void
private function errorMsg(string $message): void
{
fwrite(STDERR, $message . "\n");
}
+159 -40
View File
@@ -88,45 +88,76 @@ require_once $autoloader;
*/
const COMMAND_MAP = [
// Automation
'sync' => 'api/automation/bulk_sync.php',
'sync' => 'automation/bulk_sync.php',
// Maintenance
'inventory' => 'api/maintenance/update_repo_inventory.php',
'inventory' => 'maintenance/update_repo_inventory.php',
// Validation — general
'health' => 'api/validate/check_repo_health.php',
'check:syntax' => 'api/validate/check_php_syntax.php',
'check:version' => 'api/validate/check_version_consistency.php',
'check:changelog' => 'api/validate/check_changelog.php',
'check:structure' => 'api/validate/check_structure.php',
'check:headers' => 'api/validate/check_license_headers.php',
'check:secrets' => 'api/validate/check_no_secrets.php',
'check:tabs' => 'api/validate/check_tabs.php',
'check:paths' => 'api/validate/check_paths.php',
'check:xml' => 'api/validate/check_xml_wellformed.php',
'check:enterprise' => 'api/validate/check_enterprise_readiness.php',
'health' => 'validate/check_repo_health.php',
'check:syntax' => 'validate/check_php_syntax.php',
'check:version' => 'validate/check_version_consistency.php',
'check:changelog' => 'validate/check_changelog.php',
'check:structure' => 'validate/check_structure.php',
'check:headers' => 'validate/check_license_headers.php',
'check:secrets' => 'validate/check_no_secrets.php',
'check:tabs' => 'validate/check_tabs.php',
'check:paths' => 'validate/check_paths.php',
'check:xml' => 'validate/check_xml_wellformed.php',
'check:enterprise' => 'validate/check_enterprise_readiness.php',
// Validation — platform-specific
'check:dolibarr' => 'api/validate/check_dolibarr_module.php',
'check:joomla' => 'api/validate/check_joomla_manifest.php',
'check:language' => 'api/validate/check_language_structure.php',
'check:dolibarr' => 'validate/check_dolibarr_module.php',
'check:joomla' => 'validate/check_joomla_manifest.php',
'check:language' => 'validate/check_language_structure.php',
'check:client' => 'validate/check_client_theme.php',
'check:wiki' => 'validate/check_wiki_health.php',
// Detection
'detect' => 'api/validate/auto_detect_platform.php',
'detect' => 'validate/auto_detect_platform.php',
// Org-wide
'drift' => 'api/validate/scan_drift.php',
'drift' => 'validate/scan_drift.php',
// Release
'release' => 'api/cli/release.php',
'release' => 'cli/release.php',
'release:notes' => 'cli/release_notes.php',
'release:validate' => 'cli/release_validate.php',
'release:cascade' => 'cli/release_cascade.php',
'release:manage' => 'cli/release_manage.php',
// CLI utilities (used by workflows — centralized logic)
'version:read' => 'api/cli/version_read.php',
'version:bump' => 'api/cli/version_bump.php',
'version:propagate' => 'api/maintenance/update_version_from_readme.php',
'version:set-platform' => 'api/cli/version_set_platform.php',
'platform:detect' => 'api/cli/platform_detect.php',
'release:notes' => 'api/cli/release_notes.php',
// Version management
'version:read' => 'cli/version_read.php',
'version:bump' => 'cli/version_bump.php',
'version:propagate' => 'maintenance/update_version_from_readme.php',
'version:set-platform' => 'cli/version_set_platform.php',
// Build & package
'build:package' => 'cli/package_build.php',
'build:joomla' => 'cli/joomla_build.php',
'build:updates-xml' => 'cli/updates_xml_build.php',
// Platform detection
'platform:detect' => 'cli/platform_detect.php',
'manifest:read' => 'cli/manifest_read.php',
// Repository management
'repo:create' => 'cli/create_repo.php',
'repo:archive' => 'cli/archive_repo.php',
'repo:scaffold-client' => 'cli/scaffold_client.php',
'repo:provision' => 'cli/client_provision.php',
// Bulk operations
'bulk:push-workflow' => 'cli/bulk_workflow_push.php',
'bulk:trigger' => 'cli/bulk_workflow_trigger.php',
'bulk:sync-rulesets' => 'cli/sync_rulesets.php',
// Monitoring & dashboards
'dashboard' => 'cli/client_dashboard.php',
'grafana' => 'cli/grafana_dashboard.php',
'client:inventory' => 'cli/client_inventory.php',
// Module validation
'validate:module' => 'bin/validate-module',
];
@@ -210,24 +241,112 @@ function printCommandList(): void
{
echo "Available commands:\n\n";
$groups = [
'Automation' => ['sync'],
'Maintenance' => ['inventory'],
'Validation (general)' => ['health', 'check:syntax', 'check:version', 'check:changelog',
'check:structure', 'check:headers', 'check:secrets',
'check:tabs', 'check:paths', 'check:xml', 'check:enterprise'],
'Validation (platform)' => ['check:dolibarr', 'check:joomla', 'check:language', 'detect'],
'Organisation-wide' => ['drift'],
];
// Auto-group by command prefix or comment-based sections
$groups = [];
foreach (COMMAND_MAP as $cmd => $path) {
if (str_contains($cmd, ':')) {
$prefix = explode(':', $cmd)[0];
$groupName = match ($prefix) {
'check' => 'Validation',
'version' => 'Version',
'release' => 'Release',
'build' => 'Build',
'platform', 'manifest' => 'Platform',
'repo' => 'Repository',
'bulk' => 'Bulk Operations',
'client' => 'Client Management',
'validate' => 'Module Validation',
default => ucfirst($prefix),
};
} else {
$groupName = match ($cmd) {
'sync' => 'Automation',
'inventory' => 'Maintenance',
'health' => 'Validation',
'detect', 'drift' => 'Validation',
'dashboard', 'grafana' => 'Monitoring',
default => 'Other',
};
}
$groups[$groupName][$cmd] = $path;
}
// Load plugin commands
$pluginCommands = loadPluginCommands();
if (!empty($pluginCommands)) {
foreach ($pluginCommands as $cmd => $info) {
$type = $info['plugin'] ?? 'Plugin';
$groups["Plugin: {$type}"][$cmd] = $info['description'] ?? '';
}
}
ksort($groups);
foreach ($groups as $group => $commands) {
echo " {$group}:\n";
foreach ($commands as $cmd) {
printf(" %-22s %s\n", $cmd, COMMAND_MAP[$cmd]);
echo " \033[1m{$group}\033[0m\n";
ksort($commands);
foreach ($commands as $cmd => $path) {
printf(" \033[36m%-26s\033[0m %s\n", $cmd, basename($path));
}
echo "\n";
}
echo "Run: php bin/moko <command> --help for command-specific options.\n";
echo "All platforms: php bin/moko <command>\n";
$total = count(COMMAND_MAP) + count($pluginCommands);
echo "{$total} command(s) available.\n";
echo "Run: php bin/moko <command> --help\n";
}
/**
* Load commands from registered plugins.
*
* @return array<string, array{plugin: string, description: string, script: string}>
*/
function loadPluginCommands(): array
{
$pluginDir = dirname(__DIR__) . '/lib/Enterprise/Plugins';
if (!is_dir($pluginDir)) {
return [];
}
$commands = [];
foreach (glob("{$pluginDir}/*Plugin.php") as $file) {
$className = 'MokoEnterprise\\Plugins\\'
. pathinfo($file, PATHINFO_FILENAME);
if (!class_exists($className)) {
continue;
}
try {
$ref = new \ReflectionClass($className);
if ($ref->isAbstract()) {
continue;
}
$plugin = $ref->newInstanceWithoutConstructor();
$pluginCmds = $plugin->getCommands();
foreach ($pluginCmds as $cmd) {
$name = $cmd['name'] ?? '';
if ($name === '') {
continue;
}
$type = method_exists($plugin, 'getProjectType')
? $plugin->getProjectType() : 'unknown';
$commands[$name] = [
'plugin' => $type,
'description' => $cmd['description'] ?? '',
'script' => $cmd['script'] ?? '',
];
}
} catch (\Throwable $e) {
// Skip plugins that can't be instantiated
continue;
}
}
return $commands;
}
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/client_health_check.php
* BRIEF: Verify a client site's update server, installed version, and release availability
*
* Usage:
* php client_health_check.php --update-url URL
* php client_health_check.php --path /repo --github-output
*
* Options:
* --path Repository root (reads update server URL from manifest)
* --update-url Update server XML URL (overrides manifest)
* --site-url Live site URL for version checking via Joomla API (optional)
* --api-token Joomla API token for site-url (optional)
* --github-output Export results to $GITHUB_OUTPUT
*/
declare(strict_types=1);
$path = '.';
$updateUrl = null;
$siteUrl = null;
$apiToken = null;
$ghOutput = false;
foreach ($argv as $i => $arg) {
if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1];
if ($arg === '--update-url' && isset($argv[$i + 1])) $updateUrl = $argv[$i + 1];
if ($arg === '--site-url' && isset($argv[$i + 1])) $siteUrl = $argv[$i + 1];
if ($arg === '--api-token' && isset($argv[$i + 1])) $apiToken = $argv[$i + 1];
if ($arg === '--github-output') $ghOutput = true;
}
$root = realpath($path) ?: $path;
$checks = [];
// ── Resolve update server URL from manifest ─────────────────────────────
if ($updateUrl === null) {
$searchDirs = ["{$root}/src", $root];
foreach ($searchDirs as $dir) {
if (!is_dir($dir)) continue;
foreach (glob("{$dir}/*.xml") ?: [] as $f) {
$xml = file_get_contents($f);
if (preg_match('/<server[^>]*>([^<]+)<\/server>/', $xml, $m)) {
$updateUrl = trim($m[1]);
break 2;
}
}
}
}
if ($updateUrl === null) {
fwrite(STDERR, "No update server URL found. Use --update-url or provide a manifest with <updateservers>.\n");
exit(1);
}
echo "Update server: {$updateUrl}\n\n";
// ── Check 1: Update server accessible ───────────────────────────────────
echo "--- Update Server ---\n";
$ch = curl_init($updateUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTPHEADER => ['User-Agent: MokoHealthCheck/1.0'],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200 && !empty($response)) {
echo " PASS: HTTP {$httpCode}, " . strlen($response) . " bytes\n";
$checks['update_server'] = 'pass';
} else {
echo " FAIL: HTTP {$httpCode}\n";
$checks['update_server'] = 'fail';
}
// ── Check 2: Parse updates.xml for stable version ───────────────────────
$stableVersion = null;
$downloadUrl = null;
if (!empty($response)) {
$sections = preg_split('/<update>/', $response);
foreach ($sections as $section) {
if (strpos($section, '<tag>stable</tag>') !== false) {
if (preg_match('/<version>([^<]+)<\/version>/', $section, $m)) {
$stableVersion = $m[1];
}
if (preg_match('/<downloadurl[^>]*>([^<]+)<\/downloadurl>/', $section, $m)) {
$downloadUrl = trim($m[1]);
}
break;
}
}
if ($stableVersion === null && preg_match('/<version>([^<]+)<\/version>/', $response, $m)) {
$stableVersion = $m[1];
}
}
echo "\n--- Stable Release ---\n";
if ($stableVersion !== null) {
echo " Version: {$stableVersion}\n";
$checks['stable_version'] = $stableVersion;
} else {
echo " FAIL: Could not parse stable version\n";
$checks['stable_version'] = 'fail';
}
// ── Check 3: Download URL accessible ────────────────────────────────────
if ($downloadUrl !== null) {
echo "\n--- Download URL ---\n";
$ch = curl_init($downloadUrl);
curl_setopt_array($ch, [
CURLOPT_NOBODY => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_FOLLOWLOCATION => true,
]);
curl_exec($ch);
$dlCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$dlSize = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
curl_close($ch);
if ($dlCode === 200) {
$sizeKb = $dlSize > 0 ? round($dlSize / 1024) . 'KB' : 'unknown size';
echo " PASS: HTTP {$dlCode}, {$sizeKb}\n";
$checks['download'] = 'pass';
} else {
echo " FAIL: HTTP {$dlCode}\n";
$checks['download'] = 'fail';
}
}
// ── Check 4: Site version (optional) ────────────────────────────────────
if ($siteUrl !== null && $apiToken !== null) {
echo "\n--- Site Version ---\n";
$apiUrl = rtrim($siteUrl, '/') . '/api/index.php/v1/extensions?filter[type]=file';
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
"X-Joomla-Token: {$apiToken}",
'Accept: application/json',
],
]);
$siteResponse = curl_exec($ch);
$siteCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($siteCode === 200) {
echo " API accessible (HTTP {$siteCode})\n";
$checks['site_api'] = 'pass';
} else {
echo " WARN: Site API returned HTTP {$siteCode}\n";
$checks['site_api'] = 'warn';
}
}
// ── Summary ─────────────────────────────────────────────────────────────
echo "\n=== Health Check Summary ===\n";
$failed = 0;
foreach ($checks as $name => $result) {
$icon = ($result === 'fail') ? 'FAIL' : (($result === 'warn') ? 'WARN' : 'OK');
if ($result === 'fail') $failed++;
echo " {$icon}: {$name} = {$result}\n";
}
if ($ghOutput) {
$ghFile = getenv('GITHUB_OUTPUT');
if ($ghFile) {
file_put_contents($ghFile, "health_status=" . ($failed > 0 ? 'fail' : 'pass') . "\n", FILE_APPEND);
file_put_contents($ghFile, "health_version=" . ($stableVersion ?? 'unknown') . "\n", FILE_APPEND);
file_put_contents($ghFile, "health_failures={$failed}\n", FILE_APPEND);
}
}
exit($failed > 0 ? 1 : 0);
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/joomla_compat_check.php
* BRIEF: Check if extension targetplatform regex matches the latest Joomla version
*
* Usage:
* php joomla_compat_check.php --path /repo
* php joomla_compat_check.php --path /repo --github-output
*
* Options:
* --path Repository root (default: .)
* --github-output Export results to $GITHUB_OUTPUT
*/
declare(strict_types=1);
$path = '.';
$ghOutput = false;
foreach ($argv as $i => $arg) {
if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1];
if ($arg === '--github-output') $ghOutput = true;
}
$root = realpath($path) ?: $path;
// ── Find manifest and extract targetplatform ────────────────────────────
$manifest = null;
$searchDirs = ["{$root}/src", $root];
foreach ($searchDirs as $dir) {
if (!is_dir($dir)) continue;
foreach (glob("{$dir}/*.xml") ?: [] as $f) {
$xml = file_get_contents($f);
if (strpos($xml, '<extension') !== false && strpos($xml, 'targetplatform') !== false) {
$manifest = $f;
break 2;
}
}
}
if ($manifest === null) {
fwrite(STDERR, "No manifest with targetplatform found\n");
exit(1);
}
$xml = file_get_contents($manifest);
$relManifest = str_replace($root . '/', '', $manifest);
// Extract targetplatform version regex
$targetRegex = '';
if (preg_match('/targetplatform[^>]*version="([^"]+)"/', $xml, $m)) {
$targetRegex = $m[1];
}
if (empty($targetRegex)) {
echo "No targetplatform version found in {$relManifest}\n";
exit(1);
}
echo "Manifest: {$relManifest}\n";
echo "Target regex: {$targetRegex}\n";
// ── Fetch latest Joomla version ─────────────────────────────────────────
$joomlaVersions = [];
$updateUrl = 'https://update.joomla.org/core/sts/list_sts.xml';
$updateXml = @file_get_contents($updateUrl);
if ($updateXml === false) {
// Fallback: try the LTS feed
$updateUrl = 'https://update.joomla.org/core/list.xml';
$updateXml = @file_get_contents($updateUrl);
}
if ($updateXml !== false) {
// Parse all version entries
preg_match_all('/<version>([^<]+)<\/version>/', $updateXml, $matches);
$joomlaVersions = $matches[1] ?? [];
}
if (empty($joomlaVersions)) {
echo "WARNING: Could not fetch Joomla versions from update server\n";
echo "Tested URL: {$updateUrl}\n";
exit(0);
}
// Sort and get latest
usort($joomlaVersions, 'version_compare');
$latestJoomla = end($joomlaVersions);
echo "Latest Joomla: {$latestJoomla}\n";
// ── Test compatibility ──────────────────────────────────────────────────
// The targetplatform regex uses Joomla's regex format
// Common patterns: "5\.[0-9]+" or "((5.[0-9])|(6.[0-9]))"
$compatible = @preg_match("/{$targetRegex}/", $latestJoomla);
if ($compatible === false) {
echo "ERROR: Invalid regex in targetplatform: {$targetRegex}\n";
$result = 'error';
} elseif ($compatible === 1) {
echo "PASS: Joomla {$latestJoomla} matches targetplatform regex\n";
$result = 'pass';
} else {
// Check which major versions are supported
$supported = [];
foreach (['5.0', '5.1', '5.2', '5.3', '5.4', '6.0', '6.1', '6.2', '7.0'] as $v) {
if (@preg_match("/{$targetRegex}/", $v)) {
$supported[] = $v;
}
}
echo "WARN: Joomla {$latestJoomla} does NOT match targetplatform regex\n";
echo "Supported versions: " . implode(', ', $supported) . "\n";
echo "Consider updating targetplatform to include Joomla {$latestJoomla}\n";
$result = 'warn';
}
// ── Export ───────────────────────────────────────────────────────────────
if ($ghOutput) {
$ghFile = getenv('GITHUB_OUTPUT');
if ($ghFile) {
file_put_contents($ghFile, "compat_result={$result}\n", FILE_APPEND);
file_put_contents($ghFile, "compat_joomla={$latestJoomla}\n", FILE_APPEND);
file_put_contents($ghFile, "compat_regex={$targetRegex}\n", FILE_APPEND);
}
}
exit($result === 'error' ? 1 : 0);
+5 -4
View File
@@ -24,9 +24,9 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use MokoEnterprise\{ApiClient, AuditLogger, CLIApp, Config, PlatformAdapterFactory};
use MokoEnterprise\{ApiClient, AuditLogger, CliFramework, Config, PlatformAdapterFactory};
class JoomlaRelease extends CLIApp
class JoomlaRelease extends CliFramework
{
private const VERSION = '04.06.00';
private const ORG = 'mokoconsulting-tech';
@@ -49,6 +49,7 @@ class JoomlaRelease extends CLIApp
private ApiClient $api;
private AuditLogger $logger;
private \MokoEnterprise\GitPlatformAdapter $adapter;
protected function configure(): void
{
@@ -498,5 +499,5 @@ class JoomlaRelease extends CLIApp
}
}
$script = new JoomlaRelease('joomla_release', 'Joomla release pipeline');
exit($script->execute());
$app = new JoomlaRelease();
exit($app->execute());
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: moko-platform.CLI
* INGROUP: moko-platform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /cli/theme_lint.php
* BRIEF: Lint theme files — CSS syntax, image sizes, hardcoded URLs
*
* Usage:
* php theme_lint.php --path /repo
* php theme_lint.php --path /repo --max-image-kb 500
* php theme_lint.php --path /repo --github-output
*
* Options:
* --path Repository root (default: .)
* --max-image-kb Maximum image file size in KB (default: 500)
* --github-output Export results to $GITHUB_OUTPUT
* --strict Exit 1 on any warning (default: only on errors)
*/
declare(strict_types=1);
$path = '.';
$maxImageKb = 500;
$ghOutput = false;
$strict = false;
foreach ($argv as $i => $arg) {
if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1];
if ($arg === '--max-image-kb' && isset($argv[$i + 1])) $maxImageKb = (int)$argv[$i + 1];
if ($arg === '--github-output') $ghOutput = true;
if ($arg === '--strict') $strict = true;
}
$root = realpath($path) ?: $path;
$errors = 0;
$warnings = 0;
// ── Find source directory ───────────────────────────────────────────────
$srcDir = null;
foreach (['src', 'htdocs'] as $d) {
if (is_dir("{$root}/{$d}")) { $srcDir = "{$root}/{$d}"; break; }
}
if ($srcDir === null) {
fwrite(STDERR, "No src/ or htdocs/ directory in {$root}\n");
exit(1);
}
echo "Theme Lint: {$srcDir}\n\n";
// ── Check 1: CSS syntax validation ──────────────────────────────────────
echo "--- CSS Syntax ---\n";
$cssFiles = findFiles($srcDir, '*.css');
$cssMinFiles = findFiles($srcDir, '*.min.css');
$cssToCheck = array_diff($cssFiles, $cssMinFiles);
if (empty($cssToCheck)) {
echo " No CSS files to check\n";
} else {
foreach ($cssToCheck as $file) {
$content = file_get_contents($file);
$relPath = str_replace($root . '/', '', $file);
// Check for unmatched braces
$openBraces = substr_count($content, '{');
$closeBraces = substr_count($content, '}');
if ($openBraces !== $closeBraces) {
echo " ERROR: {$relPath}: unmatched braces (open={$openBraces}, close={$closeBraces})\n";
$errors++;
}
// Check for empty rules
if (preg_match_all('/\{[\s]*\}/', $content, $m)) {
$count = count($m[0]);
echo " WARN: {$relPath}: {$count} empty rule(s)\n";
$warnings++;
}
// Check for !important abuse (more than 10 in one file)
$importantCount = substr_count($content, '!important');
if ($importantCount > 10) {
echo " WARN: {$relPath}: {$importantCount} !important declarations (consider refactoring)\n";
$warnings++;
}
}
if ($errors === 0) {
echo " OK: " . count($cssToCheck) . " CSS file(s) checked\n";
}
}
// ── Check 2: Image file sizes ───────────────────────────────────────────
echo "\n--- Image Sizes (max {$maxImageKb}KB) ---\n";
$imageExts = ['*.jpg', '*.jpeg', '*.png', '*.gif', '*.webp', '*.svg', '*.bmp'];
$images = [];
foreach ($imageExts as $ext) {
$images = array_merge($images, findFiles($srcDir, $ext));
}
// Also check root images/ directory
if (is_dir("{$root}/images")) {
foreach ($imageExts as $ext) {
$images = array_merge($images, findFiles("{$root}/images", $ext));
}
}
$oversized = 0;
$totalSize = 0;
foreach ($images as $file) {
$size = filesize($file);
$totalSize += $size;
$relPath = str_replace($root . '/', '', $file);
$sizeKb = round($size / 1024);
if ($sizeKb > $maxImageKb) {
echo " WARN: {$relPath}: {$sizeKb}KB (exceeds {$maxImageKb}KB limit)\n";
$oversized++;
$warnings++;
}
}
$totalMb = round($totalSize / 1024 / 1024, 1);
echo " " . count($images) . " image(s), {$totalMb}MB total";
if ($oversized > 0) {
echo ", {$oversized} oversized";
}
echo "\n";
// ── Check 3: Hardcoded URLs in CSS/JS ───────────────────────────────────
echo "\n--- Hardcoded URLs ---\n";
$codeFiles = array_merge(
findFiles($srcDir, '*.css'),
findFiles($srcDir, '*.js')
);
// Exclude minified files
$codeFiles = array_filter($codeFiles, function($f) {
return !preg_match('/\.min\.(css|js)$/', $f);
});
$urlPatterns = [
'/https?:\/\/clarksvillefurs\.com/' => 'hardcoded production URL',
'/https?:\/\/[a-z]+\.dev\.mokoconsulting\.tech/' => 'hardcoded dev URL',
'/https?:\/\/localhost/' => 'localhost reference',
];
$urlIssues = 0;
foreach ($codeFiles as $file) {
$content = file_get_contents($file);
$relPath = str_replace($root . '/', '', $file);
foreach ($urlPatterns as $pattern => $desc) {
if (preg_match_all($pattern, $content, $matches)) {
$count = count($matches[0]);
echo " WARN: {$relPath}: {$count} {$desc}\n";
$urlIssues++;
$warnings++;
}
}
}
if ($urlIssues === 0) {
echo " OK: No hardcoded URLs found\n";
}
// ── Summary ─────────────────────────────────────────────────────────────
echo "\n=== Summary ===\n";
echo "Errors: {$errors}\n";
echo "Warnings: {$warnings}\n";
if ($ghOutput) {
$ghFile = getenv('GITHUB_OUTPUT');
if ($ghFile) {
file_put_contents($ghFile, "lint_errors={$errors}\n", FILE_APPEND);
file_put_contents($ghFile, "lint_warnings={$warnings}\n", FILE_APPEND);
file_put_contents($ghFile, "lint_images=" . count($images) . "\n", FILE_APPEND);
file_put_contents($ghFile, "lint_css=" . count($cssToCheck) . "\n", FILE_APPEND);
}
}
if ($errors > 0) {
exit(1);
}
if ($strict && $warnings > 0) {
exit(1);
}
exit(0);
// ── Helper: recursively find files matching a glob pattern ──────────────
function findFiles(string $dir, string $pattern): array
{
$results = [];
if (!is_dir($dir)) return $results;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if (fnmatch($pattern, $file->getFilename())) {
$results[] = $file->getPathname();
}
}
return $results;
}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "mokoconsulting-tech/enterprise",
"description": "MokoStandards Enterprise API \u2014 PHP implementation",
"type": "library",
"version": "06.00.00",
"version": "07.00.00",
"license": "GPL-3.0-or-later",
"authors": [
{
+2 -2
View File
@@ -261,9 +261,9 @@ class ApiClient
* @throws RateLimitExceeded
* @throws CircuitBreakerOpen
*/
public function delete(string $endpoint): array
public function delete(string $endpoint, ?array $body = null): array
{
return $this->request('DELETE', $endpoint);
return $this->request('DELETE', $endpoint, $body);
}
/**
+247
View File
@@ -0,0 +1,247 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.Enterprise
* INGROUP: MokoStandards.Enterprise
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /lib/Enterprise/ConfigValidator.php
* BRIEF: Validate project config against plugin JSON schema
*/
declare(strict_types=1);
namespace MokoEnterprise;
class ConfigValidator
{
/** @var array<int, string> */
private array $errors = [];
/** @var array<int, string> */
private array $warnings = [];
/**
* Validate config data against a JSON schema.
*
* @param array<string, mixed> $config Config to validate
* @param array<string, mixed> $schema JSON Schema definition
* @return bool True if valid
*/
public function validate(array $config, array $schema): bool
{
$this->errors = [];
$this->warnings = [];
$this->validateNode($config, $schema, '');
return empty($this->errors);
}
/** @return array<int, string> */
public function getErrors(): array
{
return $this->errors;
}
/** @return array<int, string> */
public function getWarnings(): array
{
return $this->warnings;
}
/**
* @param mixed $data
* @param array<string, mixed> $schema
*/
private function validateNode(
mixed $data,
array $schema,
string $path
): void {
$type = $schema['type'] ?? null;
if ($type !== null && !$this->checkType($data, $type)) {
$actual = gettype($data);
$this->errors[] = $path === ''
? "Root must be {$type}, got {$actual}"
: "{$path}: expected {$type}, got {$actual}";
return;
}
if ($type === 'object') {
$this->validateObject($data, $schema, $path);
}
if ($type === 'array' && isset($schema['items'])) {
$this->validateArray($data, $schema, $path);
}
if (isset($schema['enum'])) {
$this->validateEnum($data, $schema['enum'], $path);
}
if ($type === 'string') {
$this->validateString($data, $schema, $path);
}
if ($type === 'integer' || $type === 'number') {
$this->validateNumber($data, $schema, $path);
}
}
/**
* @param array<string, mixed> $data
* @param array<string, mixed> $schema
*/
private function validateObject(
array $data,
array $schema,
string $path
): void {
$properties = $schema['properties'] ?? [];
$required = $schema['required'] ?? [];
foreach ($required as $field) {
if (!array_key_exists($field, $data)) {
$fieldPath = $path === '' ? $field : "{$path}.{$field}";
$this->errors[] = "{$fieldPath}: required field missing";
}
}
foreach ($properties as $field => $fieldSchema) {
if (!array_key_exists($field, $data)) {
continue;
}
$fieldPath = $path === '' ? $field : "{$path}.{$field}";
$this->validateNode($data[$field], $fieldSchema, $fieldPath);
}
$known = array_keys($properties);
foreach (array_keys($data) as $field) {
if (!in_array($field, $known, true)) {
$fieldPath = $path === '' ? $field : "{$path}.{$field}";
$this->warnings[] = "{$fieldPath}: unknown property";
}
}
}
/**
* @param array<int, mixed> $data
* @param array<string, mixed> $schema
*/
private function validateArray(
array $data,
array $schema,
string $path
): void {
$itemSchema = $schema['items'];
foreach ($data as $i => $item) {
$this->validateNode(
$item,
$itemSchema,
"{$path}[{$i}]"
);
}
if (
isset($schema['minItems'])
&& count($data) < $schema['minItems']
) {
$this->errors[] = "{$path}: "
. "needs at least {$schema['minItems']} items";
}
}
/**
* @param mixed $data
* @param array<int, mixed> $allowed
*/
private function validateEnum(
mixed $data,
array $allowed,
string $path
): void {
if (!in_array($data, $allowed, true)) {
$values = implode(', ', $allowed);
$label = $path ?: 'value';
$this->errors[] = "{$label}: "
. "'{$data}' not in [{$values}]";
}
}
/**
* @param array<string, mixed> $schema
*/
private function validateString(
mixed $data,
array $schema,
string $path
): void {
if (!is_string($data)) {
return;
}
if (
isset($schema['minLength'])
&& strlen($data) < $schema['minLength']
) {
$this->errors[] = "{$path}: "
. "too short (min {$schema['minLength']})";
}
if (
isset($schema['pattern'])
&& !preg_match('/' . $schema['pattern'] . '/', $data)
) {
$this->errors[] = "{$path}: "
. "does not match pattern {$schema['pattern']}";
}
}
/**
* @param array<string, mixed> $schema
*/
private function validateNumber(
mixed $data,
array $schema,
string $path
): void {
if (!is_numeric($data)) {
return;
}
if (isset($schema['minimum']) && $data < $schema['minimum']) {
$this->errors[] = "{$path}: "
. "below minimum {$schema['minimum']}";
}
if (isset($schema['maximum']) && $data > $schema['maximum']) {
$this->errors[] = "{$path}: "
. "above maximum {$schema['maximum']}";
}
}
private function checkType(mixed $data, string $type): bool
{
return match ($type) {
'object' => is_array($data),
'array' => is_array($data)
&& array_is_list($data),
'string' => is_string($data),
'integer' => is_int($data),
'number' => is_int($data) || is_float($data),
'boolean' => is_bool($data),
'null' => is_null($data),
default => true,
};
}
}
-4
View File
@@ -16,10 +16,6 @@ parameters:
analyseAndScan:
- vendor
- node_modules (?)
# Legacy CLIApp scripts — need migration to CliFramework
- automation/repo_cleanup.php
- automation/push_files.php
- cli/joomla_release.php
reportUnmatchedIgnoredErrors: false
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
-->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
</phpunit>
+141
View File
@@ -0,0 +1,141 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
declare(strict_types=1);
namespace MokoStandards\Tests\Unit;
use MokoEnterprise\ConfigValidator;
use PHPUnit\Framework\TestCase;
class ConfigValidatorTest extends TestCase
{
private ConfigValidator $validator;
protected function setUp(): void
{
$this->validator = new ConfigValidator();
}
public function testValidConfigPasses(): void
{
$schema = [
'type' => 'object',
'properties' => [
'name' => ['type' => 'string'],
'version' => ['type' => 'string'],
],
'required' => ['name'],
];
$config = ['name' => 'MyProject', 'version' => '1.0'];
$this->assertTrue($this->validator->validate($config, $schema));
$this->assertEmpty($this->validator->getErrors());
}
public function testMissingRequiredField(): void
{
$schema = [
'type' => 'object',
'properties' => [
'name' => ['type' => 'string'],
],
'required' => ['name'],
];
$this->assertFalse($this->validator->validate([], $schema));
$this->assertStringContainsString(
'required',
$this->validator->getErrors()[0]
);
}
public function testEnumValidation(): void
{
$schema = [
'type' => 'object',
'properties' => [
'type' => [
'type' => 'string',
'enum' => ['component', 'module', 'plugin'],
],
],
];
$valid = ['type' => 'component'];
$this->assertTrue($this->validator->validate($valid, $schema));
$invalid = ['type' => 'banana'];
$this->assertFalse($this->validator->validate($invalid, $schema));
}
public function testNestedObjectValidation(): void
{
$schema = [
'type' => 'object',
'properties' => [
'db' => [
'type' => 'object',
'properties' => [
'host' => ['type' => 'string'],
'port' => ['type' => 'integer'],
],
'required' => ['host'],
],
],
];
$valid = ['db' => ['host' => 'localhost', 'port' => 3306]];
$this->assertTrue($this->validator->validate($valid, $schema));
$invalid = ['db' => ['port' => 3306]];
$this->assertFalse($this->validator->validate($invalid, $schema));
}
public function testUnknownPropertiesWarn(): void
{
$schema = [
'type' => 'object',
'properties' => [
'name' => ['type' => 'string'],
],
];
$config = ['name' => 'ok', 'extra' => 'unknown'];
$this->assertTrue($this->validator->validate($config, $schema));
$this->assertNotEmpty($this->validator->getWarnings());
}
public function testTypeMismatch(): void
{
$schema = [
'type' => 'object',
'properties' => [
'count' => ['type' => 'integer'],
],
];
$invalid = ['count' => 'not-a-number'];
$this->assertFalse($this->validator->validate($invalid, $schema));
}
public function testStringMinLength(): void
{
$schema = [
'type' => 'object',
'properties' => [
'name' => ['type' => 'string', 'minLength' => 3],
],
];
$short = ['name' => 'ab'];
$this->assertFalse($this->validator->validate($short, $schema));
$ok = ['name' => 'abc'];
$this->assertTrue($this->validator->validate($ok, $schema));
}
}
+164
View File
@@ -0,0 +1,164 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
declare(strict_types=1);
namespace MokoStandards\Tests\Unit;
use PHPUnit\Framework\TestCase;
/**
* Tests for cli/version_bump.php
*/
class VersionBumpTest extends TestCase
{
private string $tmpDir;
private string $script;
protected function setUp(): void
{
$this->tmpDir = sys_get_temp_dir() . '/moko-test-' . uniqid();
mkdir($this->tmpDir, 0755, true);
$this->script = dirname(__DIR__, 2) . '/cli/version_bump.php';
}
protected function tearDown(): void
{
$this->rmdir($this->tmpDir);
}
public function testPatchBump(): void
{
$this->writeReadme('01.02.03');
$output = $this->execute();
$this->assertStringContainsString('01.02.04', $output);
$this->assertReadmeVersion('01.02.04');
}
public function testPatchBumpRollover(): void
{
$this->writeReadme('01.02.99');
$this->execute();
$this->assertReadmeVersion('01.03.00');
}
public function testMinorBump(): void
{
$this->writeReadme('01.02.03');
$this->execute(['--minor']);
$this->assertReadmeVersion('01.03.00');
}
public function testMajorBump(): void
{
$this->writeReadme('01.02.03');
$this->execute(['--major']);
$this->assertReadmeVersion('02.00.00');
}
public function testBumpsFromHtmlComment(): void
{
file_put_contents(
"{$this->tmpDir}/README.md",
"<!-- VERSION: 03.05.01 -->\nSome content\n"
);
$this->execute();
$content = file_get_contents("{$this->tmpDir}/README.md");
$this->assertStringContainsString('03.05.02', $content);
$this->assertStringContainsString('Some content', $content);
}
public function testBumpsWhenXmlHasSuffix(): void
{
$this->writeReadme('01.00.00');
mkdir("{$this->tmpDir}/src", 0755, true);
file_put_contents(
"{$this->tmpDir}/src/test.xml",
'<extension type="component">'
. '<version>01.00.00-dev</version></extension>'
);
$output = $this->execute();
$this->assertStringContainsString('01.00.01', $output);
}
public function testFailsWithNoVersion(): void
{
file_put_contents(
"{$this->tmpDir}/README.md",
"# No version\n"
);
$code = 0;
$this->execute([], $code);
$this->assertSame(1, $code);
}
private function writeReadme(string $version): void
{
file_put_contents(
"{$this->tmpDir}/README.md",
"<!-- VERSION: {$version} -->\n"
);
}
private function assertReadmeVersion(string $expected): void
{
$content = file_get_contents("{$this->tmpDir}/README.md");
$this->assertMatchesRegularExpression(
'/VERSION:\s*' . preg_quote($expected, '/') . '/',
$content
);
}
/**
* @param string[] $extraArgs
*/
private function execute(
array $extraArgs = [],
int &$exitCode = 0
): string {
$cmd = ['php', $this->script, '--path', $this->tmpDir];
$cmd = array_merge($cmd, $extraArgs);
$descriptors = [
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$proc = proc_open($cmd, $descriptors, $pipes);
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($proc);
return $stdout ?: '';
}
private function rmdir(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$iter = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(
$dir,
\FilesystemIterator::SKIP_DOTS
),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iter as $file) {
$file->isDir()
? rmdir($file->getPathname())
: unlink($file->getPathname());
}
rmdir($dir);
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
declare(strict_types=1);
namespace MokoStandards\Tests\Unit;
use PHPUnit\Framework\TestCase;
/**
* Tests for cli/version_read.php
*/
class VersionReadTest extends TestCase
{
private string $tmpDir;
private string $script;
protected function setUp(): void
{
$this->tmpDir = sys_get_temp_dir() . '/moko-test-' . uniqid();
mkdir($this->tmpDir, 0755, true);
$this->script = dirname(__DIR__, 2) . '/cli/version_read.php';
}
protected function tearDown(): void
{
$this->rmdir($this->tmpDir);
}
public function testReadsVersionFromReadme(): void
{
file_put_contents(
"{$this->tmpDir}/README.md",
"# Test\n<!-- VERSION: 02.03.04 -->\n"
);
$this->assertSame('02.03.04', trim($this->runScript()));
}
public function testReadsVersionFromXmlManifest(): void
{
mkdir("{$this->tmpDir}/src", 0755, true);
file_put_contents(
"{$this->tmpDir}/src/test.xml",
'<extension type="component">'
. '<version>05.01.00</version></extension>'
);
$this->assertSame('05.01.00', trim($this->runScript()));
}
public function testStripsStabilitySuffixFromXml(): void
{
mkdir("{$this->tmpDir}/src", 0755, true);
file_put_contents(
"{$this->tmpDir}/src/test.xml",
'<extension type="component">'
. '<version>01.00.00-dev</version></extension>'
);
$this->assertSame('01.00.00', trim($this->runScript()));
}
public function testReturnsHigherOfReadmeAndManifest(): void
{
file_put_contents(
"{$this->tmpDir}/README.md",
"<!-- VERSION: 01.02.00 -->\n"
);
mkdir("{$this->tmpDir}/src", 0755, true);
file_put_contents(
"{$this->tmpDir}/src/test.xml",
'<extension type="component">'
. '<version>01.03.00</version></extension>'
);
$this->assertSame('01.03.00', trim($this->runScript()));
}
public function testExitsNonZeroWhenNoVersion(): void
{
file_put_contents(
"{$this->tmpDir}/README.md",
"# No version here\n"
);
$code = 0;
$this->runScript($code);
$this->assertSame(1, $code);
}
private function runScript(int &$exitCode = 0): string
{
$proc = proc_open(
['php', $this->script, '--path', $this->tmpDir],
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes
);
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($proc);
return $stdout ?: '';
}
private function rmdir(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$iter = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(
$dir,
\FilesystemIterator::SKIP_DOTS
),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iter as $file) {
$file->isDir()
? rmdir($file->getPathname())
: unlink($file->getPathname());
}
rmdir($dir);
}
}
+12 -13
View File
@@ -22,7 +22,7 @@ require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\{
CLIApp,
CliFramework,
ProjectTypeDetector,
PluginFactory,
PluginRegistry,
@@ -36,7 +36,7 @@ use MokoEnterprise\{
* Detects whether a repository is a Joomla/WaaS component, Dolibarr/CRM module,
* or generic repository, then validates against appropriate schema
*/
class AutoDetectPlatform extends CLIApp
class AutoDetectPlatform extends CliFramework
{
private const DETECTION_THRESHOLD = 0.5; // 50% confidence required
@@ -62,20 +62,19 @@ class AutoDetectPlatform extends CLIApp
private string $schemaFile = '';
private ?object $detectedPlugin = null;
protected function setupArguments(): array
protected function configure(): void
{
return [
'repo-path:' => 'Path to repository to analyze (default: current directory)',
'schema-dir:' => 'Path to schema definitions directory (default: definitions/default)',
'output-dir:' => 'Directory for output reports (default: var/logs/validation)',
];
$this->setDescription('Automatically detect platform type and validate repository');
$this->addArgument('--repo-path', 'Path to repository to analyze', '.');
$this->addArgument('--schema-dir', 'Path to schema definitions directory', 'definitions/default');
$this->addArgument('--output-dir', 'Directory for output reports', 'var/logs/validation');
}
protected function run(): int
{
$repoPath = $this->getOption('repo-path', '.');
$schemaDir = $this->getOption('schema-dir', 'definitions/default');
$outputDir = $this->getOption('output-dir', 'var/logs/validation');
$repoPath = $this->getArgument('--repo-path', '.');
$schemaDir = $this->getArgument('--schema-dir', 'definitions/default');
$outputDir = $this->getArgument('--output-dir', 'var/logs/validation');
// Make paths absolute
$repoPath = $this->getAbsolutePath($repoPath);
@@ -151,7 +150,7 @@ class AutoDetectPlatform extends CLIApp
}
// Output results
if ($this->jsonOutput) {
if ($this->getArgument("--json", false)) {
$this->outputJson();
} else {
$this->displayResults();
@@ -953,5 +952,5 @@ class AutoDetectPlatform extends CLIApp
}
// Run the application
$app = new AutoDetectPlatform('auto_detect_platform', 'Automatically detect platform type and validate repository');
$app = new AutoDetectPlatform();
exit($app->execute());
+15 -19
View File
@@ -17,29 +17,24 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use MokoEnterprise\CLIApp;
use MokoEnterprise\CliFramework;
class CheckWikiHealth extends CLIApp
class CheckWikiHealth extends CliFramework
{
public function __construct()
protected function configure(): void
{
parent::__construct('check-wiki-health', 'Validate wiki health for a repository', '01.00.00');
}
protected function setupArguments(): array
{
return [
'path:' => 'Repository path (default: current directory)',
'gitea-url:' => 'Gitea base URL (default: https://git.mokoconsulting.tech)',
'token:' => 'Gitea API token (or set GITEA_TOKEN env var)',
];
$this->setDescription('Validate wiki health for a repository');
$this->addArgument('--path', 'Repository path (default: current directory)', '.');
$this->addArgument('--gitea-url', 'Gitea base URL', 'https://git.mokoconsulting.tech');
$this->addArgument('--token', 'Gitea API token (or set GITEA_TOKEN env var)', '');
$this->addArgument('--json', 'Output as JSON', false);
}
protected function run(): int
{
$repoPath = realpath($this->getOption('path', '.')) ?: '.';
$giteaUrl = $this->getOption('gitea-url', 'https://git.mokoconsulting.tech');
$token = $this->getOption('token', getenv('GITEA_TOKEN') ?: '');
$repoPath = realpath($this->getArgument('--path', '.')) ?: '.';
$giteaUrl = $this->getArgument('--gitea-url', 'https://git.mokoconsulting.tech');
$token = $this->getArgument('--token', getenv('GITEA_TOKEN') ?: '');
// Detect repo owner/name from git config
$configFile = $repoPath . '/.git/config';
@@ -76,7 +71,7 @@ class CheckWikiHealth extends CLIApp
if ($pages === null) {
$this->log(' No wiki found or API error', 'WARNING');
$issues++;
if ($this->jsonOutput) {
if ($this->getArgument("--json", false)) {
echo json_encode(['status' => 'no_wiki', 'issues' => $issues]);
}
return 0;
@@ -118,7 +113,7 @@ class CheckWikiHealth extends CLIApp
}
}
if ($this->jsonOutput) {
if ($this->getArgument("--json", false)) {
echo json_encode([
'repo' => "{$owner}/{$repo}",
'pages' => $pageCount,
@@ -153,4 +148,5 @@ class CheckWikiHealth extends CLIApp
}
}
(new CheckWikiHealth())->execute();
$app = new CheckWikiHealth();
exit($app->execute());