Files
Jonathan Miller 4cc3f5bee4
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 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
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
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 2s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Release configuration (pull_request) Successful in 6s
Generic: Repo Health / Scripts governance (pull_request) Successful in 6s
Generic: Repo Health / Repository health (push) Successful in 14s
Generic: Repo Health / Repository health (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 49s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been skipped
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been skipped
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
style: fix all PHPCS PSR-12 violations across 74 files (7539 → 0 errors)
- Convert tabs to spaces (3,413 violations)
- Fix line endings, trailing whitespace, brace placement
- Break lines exceeding 150-char absolute limit
- Replace heredoc tab closers with spaces
- Fix empty elseif, forbidden function calls
- Update phpcs.xml: exclude rules inappropriate for CLI scripts
  (SideEffects, MissingNamespace, MultipleClasses, HeaderOrder,
  empty catch blocks)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 17:07:51 -05:00

671 lines
21 KiB
PHP

<?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.Plugins
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /lib/Enterprise/Plugins/MobilePlugin.php
* BRIEF: Enterprise plugin for mobile app projects
*/
declare(strict_types=1);
namespace MokoEnterprise\Plugins;
use MokoEnterprise\AbstractProjectPlugin;
/**
* Mobile App Project Plugin
*
* Provides validation, metrics, and management capabilities for
* mobile applications (React Native, Flutter, native iOS/Android).
*/
class MobilePlugin extends AbstractProjectPlugin
{
/**
* {@inheritdoc}
*/
public function getProjectType(): string
{
return 'mobile';
}
/**
* {@inheritdoc}
*/
public function getPluginName(): string
{
return 'Mobile App Enterprise Plugin';
}
/**
* {@inheritdoc}
*/
public function validateProject(array $config, string $projectPath): array
{
$errors = [];
$warnings = [];
$platform = $this->detectPlatform($projectPath);
switch ($platform) {
case 'react-native':
if (!$this->fileExists($projectPath, 'package.json')) {
$errors[] = 'React Native project missing package.json';
}
if (
!$this->fileExists($projectPath, 'app.json') &&
!$this->fileExists($projectPath, 'app.config.js')
) {
$warnings[] = 'Missing app.json or app.config.js';
}
if (
!$this->fileExists($projectPath, 'ios') &&
!$this->fileExists($projectPath, 'android')
) {
$warnings[] = 'No native platform directories found';
}
break;
case 'flutter':
if (!$this->fileExists($projectPath, 'pubspec.yaml')) {
$errors[] = 'Flutter project missing pubspec.yaml';
}
if (!$this->fileExists($projectPath, 'lib')) {
$errors[] = 'Flutter project missing lib directory';
}
break;
case 'ios':
if (
!$this->fileExists($projectPath, '*.xcodeproj') &&
!$this->fileExists($projectPath, '*.xcworkspace')
) {
$errors[] = 'iOS project missing Xcode project file';
}
if (!$this->fileExists($projectPath, 'Podfile')) {
$warnings[] = 'No Podfile found (CocoaPods not used)';
}
break;
case 'android':
if (!$this->fileExists($projectPath, 'build.gradle')) {
$errors[] = 'Android project missing build.gradle';
}
if (!$this->fileExists($projectPath, 'app/src/main')) {
$errors[] = 'Android project missing standard structure';
}
break;
}
// Check for app icons
if (!$this->hasAppIcons($projectPath, $platform)) {
$warnings[] = 'App icons not found';
}
// Check for splash screen
if (!$this->hasSplashScreen($projectPath, $platform)) {
$warnings[] = 'Splash screen not found';
}
// Check for tests
if (!$this->hasTests($projectPath, $platform)) {
$warnings[] = 'No tests found';
}
$this->log(
'Mobile project validation completed',
'info',
['errors' => count($errors), 'warnings' => count($warnings), 'platform' => $platform]
);
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings,
];
}
/**
* {@inheritdoc}
*/
public function collectMetrics(string $projectPath, array $config): array
{
$platform = $this->detectPlatform($projectPath);
$metrics = [
'platform' => $platform,
'supports_ios' => $this->supportsIOS($projectPath, $platform),
'supports_android' => $this->supportsAndroid($projectPath, $platform),
'has_app_icons' => $this->hasAppIcons($projectPath, $platform),
'has_splash_screen' => $this->hasSplashScreen($projectPath, $platform),
'has_tests' => $this->hasTests($projectPath, $platform),
'has_ci' => $this->hasCICD($projectPath),
];
// Platform-specific metrics
switch ($platform) {
case 'react-native':
$packageData = $this->parseJsonFile($projectPath, 'package.json');
$metrics['js_files'] = $this->countFiles($projectPath, '**/*.js');
$metrics['jsx_files'] = $this->countFiles($projectPath, '**/*.jsx');
$metrics['ts_files'] = $this->countFiles($projectPath, '**/*.ts');
$metrics['tsx_files'] = $this->countFiles($projectPath, '**/*.tsx');
$metrics['dependencies'] = $packageData ? count($packageData['dependencies'] ?? []) : 0;
$metrics['uses_typescript'] = $this->fileExists($projectPath, 'tsconfig.json');
$metrics['uses_expo'] = $this->usesExpo($projectPath);
break;
case 'flutter':
$metrics['dart_files'] = $this->countFiles($projectPath, '**/*.dart');
$metrics['dependencies'] = $this->countFlutterDependencies($projectPath);
break;
case 'ios':
$metrics['swift_files'] = $this->countFiles($projectPath, '**/*.swift');
$metrics['objc_files'] = $this->countFiles($projectPath, '**/*.m');
break;
case 'android':
$metrics['kotlin_files'] = $this->countFiles($projectPath, '**/*.kt');
$metrics['java_files'] = $this->countFiles($projectPath, '**/*.java');
break;
}
// Count total lines
$metrics['total_lines'] = $this->countTotalLines($projectPath, $platform);
// Record metrics
$this->recordMetric('mobile', 'platform', $platform);
$this->recordMetric('mobile', 'total_lines', $metrics['total_lines']);
$this->log('Collected mobile metrics', 'info', $metrics);
return $metrics;
}
/**
* {@inheritdoc}
*/
public function healthCheck(string $projectPath, array $config): array
{
$issues = [];
$score = 100;
$platform = $this->detectPlatform($projectPath);
// Platform-specific checks
switch ($platform) {
case 'react-native':
if (!$this->fileExists($projectPath, 'package.json')) {
$issues[] = [
'severity' => 'critical',
'message' => 'Missing package.json',
];
$score -= 30;
}
if (!$this->fileExists($projectPath, '.watchmanconfig')) {
$issues[] = [
'severity' => 'info',
'message' => 'Missing .watchmanconfig',
];
$score -= 5;
}
break;
case 'flutter':
if (!$this->fileExists($projectPath, 'pubspec.yaml')) {
$issues[] = [
'severity' => 'critical',
'message' => 'Missing pubspec.yaml',
];
$score -= 30;
}
break;
case 'ios':
if (!$this->hasIOSProject($projectPath)) {
$issues[] = [
'severity' => 'critical',
'message' => 'No Xcode project found',
];
$score -= 30;
}
break;
case 'android':
if (!$this->fileExists($projectPath, 'build.gradle')) {
$issues[] = [
'severity' => 'critical',
'message' => 'Missing build.gradle',
];
$score -= 30;
}
break;
}
// Check for app icons
if (!$this->hasAppIcons($projectPath, $platform)) {
$issues[] = [
'severity' => 'warning',
'message' => 'App icons missing',
];
$score -= 10;
}
// Check for tests
if (!$this->hasTests($projectPath, $platform)) {
$issues[] = [
'severity' => 'warning',
'message' => 'No tests found',
];
$score -= 15;
}
// Check for CI/CD
if (!$this->hasCICD($projectPath)) {
$issues[] = [
'severity' => 'info',
'message' => 'No CI/CD configuration',
];
$score -= 10;
}
// Check for README
if (!$this->fileExists($projectPath, 'README.md')) {
$issues[] = [
'severity' => 'warning',
'message' => 'Missing README.md',
];
$score -= 5;
}
// Check for .gitignore
if (!$this->fileExists($projectPath, '.gitignore')) {
$issues[] = [
'severity' => 'warning',
'message' => 'Missing .gitignore',
];
$score -= 5;
}
// Check for security best practices
if ($this->hasInsecureStorage($projectPath, $platform)) {
$issues[] = [
'severity' => 'critical',
'message' => 'Potential insecure data storage detected',
];
$score -= 20;
}
$score = max(0, $score);
$this->log('Mobile health check completed', 'info', [
'score' => $score,
'issues_count' => count($issues),
'platform' => $platform,
]);
return [
'healthy' => $score >= 70,
'score' => $score,
'issues' => $issues,
];
}
/**
* {@inheritdoc}
*/
public function getRequiredFiles(): array
{
return [
'React Native: package.json, app.json',
'Flutter: pubspec.yaml, lib/',
'iOS: *.xcodeproj or *.xcworkspace',
'Android: build.gradle, app/src/main/',
];
}
/**
* {@inheritdoc}
*/
public function getRecommendedFiles(): array
{
return [
'README.md',
'.gitignore',
'App icons for all required sizes',
'Splash screen assets',
'tests/ or __tests__/',
'.mokogitea/workflows/* or .gitea/workflows/* or fastlane/',
'React Native: metro.config.js',
'Flutter: analysis_options.yaml',
'iOS: Podfile',
'Android: proguard-rules.pro',
];
}
/**
* {@inheritdoc}
*/
public function getConfigSchema(): array
{
return [
'type' => 'object',
'properties' => [
'platform' => [
'type' => 'string',
'enum' => ['react-native', 'flutter', 'ios', 'android', 'xamarin'],
'description' => 'Mobile platform/framework',
],
'supports_ios' => [
'type' => 'boolean',
'description' => 'Project supports iOS',
],
'supports_android' => [
'type' => 'boolean',
'description' => 'Project supports Android',
],
'min_ios_version' => [
'type' => 'string',
'description' => 'Minimum iOS version',
],
'min_android_api' => [
'type' => 'integer',
'description' => 'Minimum Android API level',
],
],
'required' => ['platform'],
];
}
/**
* {@inheritdoc}
*/
public function getBestPractices(): array
{
return [
'Support both iOS and Android for wider reach',
'Implement proper error handling and crash reporting',
'Use secure storage for sensitive data',
'Implement proper app permissions handling',
'Optimize app size and performance',
'Provide app icons for all required sizes',
'Include splash screen with proper sizing',
'Implement comprehensive unit and integration tests',
'Use CI/CD for automated builds and deployments',
'Follow platform-specific design guidelines',
'Implement proper deep linking',
'Add analytics and monitoring',
'Handle offline scenarios gracefully',
'Optimize images and assets',
'Keep dependencies up to date',
];
}
/**
* Detect mobile platform
*/
private function detectPlatform(string $projectPath): string
{
// React Native
if ($this->fileExists($projectPath, 'package.json')) {
$packageData = $this->parseJsonFile($projectPath, 'package.json');
if ($packageData && isset($packageData['dependencies']['react-native'])) {
return 'react-native';
}
}
// Flutter
if ($this->fileExists($projectPath, 'pubspec.yaml')) {
return 'flutter';
}
// iOS
if ($this->hasIOSProject($projectPath)) {
return 'ios';
}
// Android
if (
$this->fileExists($projectPath, 'build.gradle') &&
$this->fileExists($projectPath, 'app/src/main')
) {
return 'android';
}
return 'unknown';
}
/**
* Check if supports iOS
*/
private function supportsIOS(string $projectPath, string $platform): bool
{
if ($platform === 'ios') {
return true;
}
return $this->fileExists($projectPath, 'ios');
}
/**
* Check if supports Android
*/
private function supportsAndroid(string $projectPath, string $platform): bool
{
if ($platform === 'android') {
return true;
}
return $this->fileExists($projectPath, 'android');
}
/**
* Check for app icons
*/
private function hasAppIcons(string $projectPath, string $platform): bool
{
switch ($platform) {
case 'react-native':
return $this->fileExists($projectPath, 'android/app/src/main/res/mipmap-*') ||
$this->fileExists($projectPath, 'ios/*/Images.xcassets/AppIcon.appiconset');
case 'flutter':
return $this->fileExists($projectPath, 'android/app/src/main/res/mipmap-*') ||
$this->fileExists($projectPath, 'ios/Runner/Assets.xcassets/AppIcon.appiconset');
case 'ios':
return $this->countFiles($projectPath, '**/AppIcon.appiconset') > 0;
case 'android':
return $this->fileExists($projectPath, 'app/src/main/res/mipmap-*');
default:
return false;
}
}
/**
* Check for splash screen
*/
private function hasSplashScreen(string $projectPath, string $platform): bool
{
switch ($platform) {
case 'react-native':
return $this->fileExists($projectPath, 'android/app/src/main/res/drawable/launch_screen*') ||
$this->fileExists($projectPath, 'ios/*/LaunchScreen*');
case 'flutter':
return $this->fileExists($projectPath, 'android/app/src/main/res/drawable/launch_background*') ||
$this->fileExists($projectPath, 'ios/Runner/Assets.xcassets/LaunchImage*');
case 'ios':
return $this->countFiles($projectPath, '**/LaunchScreen*') > 0;
case 'android':
return $this->fileExists($projectPath, 'app/src/main/res/drawable/launch_*');
default:
return false;
}
}
/**
* Check for tests
*/
private function hasTests(string $projectPath, string $platform): bool
{
switch ($platform) {
case 'react-native':
return $this->fileExists($projectPath, '__tests__') ||
$this->fileExists($projectPath, 'e2e') ||
$this->countFiles($projectPath, '**/*.test.js') > 0;
case 'flutter':
return $this->fileExists($projectPath, 'test') ||
$this->countFiles($projectPath, '**/*_test.dart') > 0;
case 'ios':
return $this->fileExists($projectPath, '*Tests') ||
$this->countFiles($projectPath, '**/*Tests.swift') > 0;
case 'android':
return $this->fileExists($projectPath, 'app/src/test') ||
$this->fileExists($projectPath, 'app/src/androidTest');
default:
return false;
}
}
/**
* Check for CI/CD
*/
private function hasCICD(string $projectPath): bool
{
return $this->fileExists($projectPath, '.mokogitea/workflows') ||
$this->fileExists($projectPath, '.mokogitea/workflows') ||
$this->fileExists($projectPath, '.gitlab-ci.yml') ||
$this->fileExists($projectPath, 'fastlane') ||
$this->fileExists($projectPath, '.circleci');
}
/**
* Check if uses Expo
*/
private function usesExpo(string $projectPath): bool
{
$packageData = $this->parseJsonFile($projectPath, 'package.json');
return $packageData && isset($packageData['dependencies']['expo']);
}
/**
* Count Flutter dependencies
*/
private function countFlutterDependencies(string $projectPath): int
{
$pubspec = $this->readFile($projectPath, 'pubspec.yaml');
if (!$pubspec) {
return 0;
}
$lines = explode("\n", $pubspec);
$inDeps = false;
$count = 0;
foreach ($lines as $line) {
if (strpos($line, 'dependencies:') !== false) {
$inDeps = true;
continue;
}
if ($inDeps && preg_match('/^\s{2}\w+:/', $line)) {
$count++;
} elseif ($inDeps && preg_match('/^\w+:/', $line)) {
break;
}
}
return $count;
}
/**
* Count total lines
*/
private function countTotalLines(string $projectPath, string $platform): int
{
$extensions = [];
switch ($platform) {
case 'react-native':
$extensions = ['js', 'jsx', 'ts', 'tsx'];
break;
case 'flutter':
$extensions = ['dart'];
break;
case 'ios':
$extensions = ['swift', 'm', 'h'];
break;
case 'android':
$extensions = ['kt', 'java'];
break;
}
$totalLines = 0;
foreach ($extensions as $ext) {
$files = $this->findFiles($projectPath, "**/*.{$ext}");
foreach ($files as $file) {
if (
is_file($file) &&
strpos($file, 'node_modules') === false &&
strpos($file, 'build') === false
) {
$totalLines += count(file($file));
}
}
}
return $totalLines;
}
/**
* Check for iOS project
*/
private function hasIOSProject(string $projectPath): bool
{
return $this->countFiles($projectPath, '*.xcodeproj') > 0 ||
$this->countFiles($projectPath, '*.xcworkspace') > 0;
}
/**
* Check for insecure storage
*/
private function hasInsecureStorage(string $projectPath, string $platform): bool
{
// Simple heuristic check - would be more comprehensive in production
switch ($platform) {
case 'react-native':
$files = $this->findFiles($projectPath, '**/*.js');
foreach (array_slice($files, 0, 10) as $file) {
if (is_file($file)) {
$content = @file_get_contents($file);
if ($content && strpos($content, 'AsyncStorage.setItem') !== false) {
// Should check if sensitive data without encryption
return true;
}
}
}
break;
}
return false;
}
}