0, 'modules_enabled' => 0, 'skipped_empty' => 0]; $message = 'module-migrate: all modules up to date, 0 new migrations.'; $step = ModuleCliRuntime::runStep('module-migrate', function () use (&$summary, &$message): int { $lockResult = ModuleCliRuntime::withFileLock( ModuleCliRuntime::projectRoot() . '/storage/runtime/.module-migrate.lock', 'module-migrate: another migration run is in progress, skipping.', function () use (&$summary, &$message): int { /** @var ModuleMigrationService $service */ $service = app(ModuleMigrationService::class); $result = $service->applyPendingMigrations(); $summary = $result; if ($result['modules_enabled'] === 0) { $message = 'module-migrate: no modules enabled, nothing to do.'; } elseif ($result['applied'] === 0) { $message = 'module-migrate: all modules up to date, 0 new migrations.'; } else { $message = sprintf('module-migrate: applied %d migration(s).', $result['applied']); } if ($result['skipped_empty'] > 0) { $message .= sprintf(' (%d empty file(s) skipped)', $result['skipped_empty']); } return $result['ok'] ? 0 : 1; } ); if ($lockResult['busy']) { $message = (string) $lockResult['message']; } return $lockResult['exit_code']; }); return [ 'command' => 'module:migrate', 'status' => $step['status'], 'exit_code' => $step['exit_code'], 'vendor_warnings_ignored' => $step['vendor_warnings_ignored'], 'message' => $step['error'] ?? $message, ]; } public function permissionsSync(): array { $sync = [ 'created' => 0, 'updated' => 0, 'unchanged' => 0, 'deactivated' => 0, 'total' => 0, ]; $message = 'module-permissions-sync: ok'; $step = ModuleCliRuntime::runStep('module-permissions-sync', function () use (&$sync, &$message): int { $lockResult = ModuleCliRuntime::withFileLock( ModuleCliRuntime::projectRoot() . '/storage/runtime/.module-permissions-sync.lock', 'module-permissions-sync: another sync is in progress, skipping.', function () use (&$sync): int { /** @var ModulePermissionSynchronizer $synchronizer */ $synchronizer = app(ModulePermissionSynchronizer::class); $sync = $synchronizer->sync(); return 0; } ); if ($lockResult['busy']) { $message = (string) $lockResult['message']; } return $lockResult['exit_code']; }); return [ 'command' => 'module:permissions-sync', 'status' => $step['status'], 'exit_code' => $step['exit_code'], 'vendor_warnings_ignored' => $step['vendor_warnings_ignored'], 'created' => (int) $sync['created'], 'updated' => (int) $sync['updated'], 'unchanged' => (int) $sync['unchanged'], 'deactivated' => (int) $sync['deactivated'], 'total' => (int) $sync['total'], 'message' => $step['error'] ?? $message, ]; } public function build(): array { $buildSummary = [ 'core_entries' => 0, 'module_entries' => 0, ]; $message = 'module-build: ok'; $step = ModuleCliRuntime::runStep('module-build', function () use (&$buildSummary, &$message): int { $runtimeDir = ModuleCliRuntime::projectRoot() . '/storage/runtime/pages'; $runtimeParent = dirname($runtimeDir); if (!is_dir($runtimeParent) && !mkdir($runtimeParent, 0775, true) && !is_dir($runtimeParent)) { $message = "Failed to create runtime directory: {$runtimeParent}"; return 1; } $lockResult = ModuleCliRuntime::withFileLock( ModuleCliRuntime::projectRoot() . '/storage/runtime/.module-build.lock', 'module-build: another build is in progress, skipping.', function () use (&$buildSummary, $runtimeDir): int { /** @var ModuleRegistry $registry */ $registry = app(ModuleRegistry::class); /** @var ModuleRuntimePageBuilder $builder */ $builder = app(ModuleRuntimePageBuilder::class); $modules = $registry->getModules(); $buildSummary = $builder->build( $runtimeDir, ModuleCliRuntime::projectRoot() . '/pages', $modules ); ModuleRuntimePageBuilder::writeFingerprint($runtimeDir, $modules); return 0; } ); if ($lockResult['busy']) { $message = (string) $lockResult['message']; } return $lockResult['exit_code']; }); return [ 'command' => 'module:build', 'status' => $step['status'], 'exit_code' => $step['exit_code'], 'vendor_warnings_ignored' => $step['vendor_warnings_ignored'], 'core_entries' => (int) $buildSummary['core_entries'], 'module_entries' => (int) $buildSummary['module_entries'], 'message' => $step['error'] ?? $message, ]; } public function assetsSync(): array { $summary = [ 'created' => 0, 'updated' => 0, 'removed' => 0, 'unchanged' => 0, ]; $mode = trim((string) getenv('APP_MODULE_ASSET_MODE')); if ($mode === '') { $mode = 'symlink'; } $message = 'module-assets-sync: ok'; $step = ModuleCliRuntime::runStep('module-assets-sync', function () use (&$summary, &$message, $mode): int { $lockResult = ModuleCliRuntime::withFileLock( ModuleCliRuntime::projectRoot() . '/storage/runtime/.module-assets-sync.lock', 'module-assets-sync: another sync is in progress, skipping.', function () use (&$summary, $mode): int { /** @var ModuleRegistry $registry */ $registry = app(ModuleRegistry::class); /** @var ModuleRuntimeAssetPublisher $publisher */ $publisher = app(ModuleRuntimeAssetPublisher::class); $summary = $publisher->publish( ModuleCliRuntime::projectRoot() . '/web/modules', $registry->getModules(), $mode ); return 0; } ); if ($lockResult['busy']) { $message = (string) $lockResult['message']; } return $lockResult['exit_code']; }); return [ 'command' => 'module:assets-sync', 'status' => $step['status'], 'exit_code' => $step['exit_code'], 'vendor_warnings_ignored' => $step['vendor_warnings_ignored'], 'mode' => $mode, 'created' => (int) $summary['created'], 'updated' => (int) $summary['updated'], 'removed' => (int) $summary['removed'], 'unchanged' => (int) $summary['unchanged'], 'message' => $step['error'] ?? $message, ]; } public function deactivate(string $moduleId, bool $confirm, bool $dryRun): array { $message = ''; $step = ModuleCliRuntime::runStep('module-deactivate', function () use ($moduleId, $confirm, $dryRun, &$message): int { if ($moduleId === '') { $message = 'Error: module id is required.'; return 1; } if (!$confirm && !$dryRun) { $message = 'Error: must pass --confirm to execute or --dry-run to validate.'; return 1; } $manifest = ModuleManifestLoader::loadFromDisk( ModuleCliRuntime::projectRoot() . '/modules', $moduleId ); $handlerClass = $manifest->deactivationHandler; if ($handlerClass === null) { $message = "Module '{$moduleId}' has no deactivation_handler declared — nothing to do."; return 0; } if (!class_exists($handlerClass)) { $message = "Error: deactivation handler class '{$handlerClass}' not found."; return 1; } $container = $GLOBALS['minty_app_container'] ?? null; if (!$container instanceof AppContainer) { $message = 'Error: app container is not initialized.'; return 1; } $handler = $container->has($handlerClass) ? $container->get($handlerClass) : new $handlerClass(); if (!$handler instanceof ModuleDeactivationHandler) { $message = "Error: '{$handlerClass}' does not implement ModuleDeactivationHandler."; return 1; } if ($dryRun) { $message = "Dry-run: handler '{$handlerClass}' found and valid for module '{$moduleId}'."; return 0; } $handler->deactivate($container); $message = "Deactivation handler for module '{$moduleId}' completed."; return 0; }); return [ 'command' => 'module:deactivate', 'status' => $step['status'], 'exit_code' => $step['exit_code'], 'vendor_warnings_ignored' => $step['vendor_warnings_ignored'], 'message' => $step['error'] ?? $message, ]; } public function runtimeSync(): array { $startedAt = microtime(true); $steps = [ 'migrate' => ['status' => 'pending', 'exit_code' => 0, 'vendor_warnings_ignored' => 0], 'permissions-sync' => ['status' => 'pending', 'exit_code' => 0, 'vendor_warnings_ignored' => 0], 'build' => ['status' => 'pending', 'exit_code' => 0, 'vendor_warnings_ignored' => 0], 'assets-sync' => ['status' => 'pending', 'exit_code' => 0, 'vendor_warnings_ignored' => 0], ]; $message = ''; $step = ModuleCliRuntime::runStep('module-runtime-sync', function () use (&$steps, &$message): int { $lockResult = ModuleCliRuntime::withFileLock( ModuleCliRuntime::projectRoot() . '/storage/runtime/.module-runtime-sync.lock', 'module-runtime-sync: another runtime sync is in progress, skipping.', function () use (&$steps): int { $orderedSteps = [ 'migrate' => fn (): array => $this->migrate(), 'permissions-sync' => fn (): array => $this->permissionsSync(), 'build' => fn (): array => $this->build(), 'assets-sync' => fn (): array => $this->assetsSync(), ]; foreach ($orderedSteps as $stepName => $runner) { $result = $runner(); $steps[$stepName] = [ 'status' => $result['status'], 'exit_code' => $result['exit_code'], 'vendor_warnings_ignored' => $result['vendor_warnings_ignored'], ]; if ($result['exit_code'] !== 0) { return $result['exit_code']; } } return 0; } ); if ($lockResult['busy']) { $message = (string) $lockResult['message']; } return $lockResult['exit_code']; }); if ($message === '') { $message = sprintf( 'module-runtime-sync: summary migrate=%s permissions-sync=%s build=%s assets-sync=%s vendor_warnings_ignored=%d', $steps['migrate']['status'], $steps['permissions-sync']['status'], $steps['build']['status'], $steps['assets-sync']['status'], $step['vendor_warnings_ignored'] ); } return [ 'command' => 'module:sync', 'status' => $step['status'], 'exit_code' => $step['exit_code'], 'vendor_warnings_ignored_total' => $step['vendor_warnings_ignored'], 'duration_ms' => max(0, (int) round((microtime(true) - $startedAt) * 1000)), 'steps' => $steps, 'message' => $step['error'] ?? $message, ]; } }