major update
This commit is contained in:
115
bin/codex-skills-sync.sh
Executable file
115
bin/codex-skills-sync.sh
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: bin/codex-skills-sync.sh [--check|--apply]
|
||||
|
||||
Options:
|
||||
--check Prüft Drift zwischen Repo-Skills und ~/.codex/skills (Default)
|
||||
--apply Synchronisiert Repo-Skills nach ~/.codex/skills
|
||||
USAGE
|
||||
}
|
||||
|
||||
mode="check"
|
||||
if [[ $# -gt 1 ]]; then
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ $# -eq 1 ]]; then
|
||||
case "$1" in
|
||||
--check)
|
||||
mode="check"
|
||||
;;
|
||||
--apply)
|
||||
mode="apply"
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] Unknown option: $1" >&2
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if ! command -v rsync >/dev/null 2>&1; then
|
||||
echo "[ERROR] rsync is required." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd -- "${script_dir}/.." && pwd)"
|
||||
source_dir="${repo_root}/tools/codex-skills"
|
||||
codex_home_dir="${CODEX_HOME:-${HOME}/.codex}"
|
||||
target_dir="${codex_home_dir}/skills"
|
||||
managed_skills=(
|
||||
"core-guardrails"
|
||||
"starterkit-planner"
|
||||
)
|
||||
|
||||
for skill in "${managed_skills[@]}"; do
|
||||
if [[ ! -d "${source_dir}/${skill}" ]]; then
|
||||
echo "[ERROR] Missing source skill: ${source_dir}/${skill}" >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
|
||||
check_skill() {
|
||||
local skill="$1"
|
||||
local src="${source_dir}/${skill}"
|
||||
local dst="${target_dir}/${skill}"
|
||||
|
||||
if [[ ! -d "${dst}" ]]; then
|
||||
echo "[DRIFT] Missing target skill: ${dst}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local diff
|
||||
diff="$(rsync -rcn --delete --itemize-changes "${src}/" "${dst}/" || true)"
|
||||
if [[ -n "${diff}" ]]; then
|
||||
echo "[DRIFT] ${skill} differs:"
|
||||
echo "${diff}" | sed 's/^/ /'
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "[OK] ${skill} is in sync"
|
||||
return 0
|
||||
}
|
||||
|
||||
apply_skill() {
|
||||
local skill="$1"
|
||||
local src="${source_dir}/${skill}"
|
||||
local dst="${target_dir}/${skill}"
|
||||
|
||||
mkdir -p "${dst}"
|
||||
rsync -a --delete "${src}/" "${dst}/"
|
||||
echo "[SYNC] ${skill} -> ${dst}"
|
||||
}
|
||||
|
||||
status=0
|
||||
|
||||
if [[ "${mode}" == "apply" ]]; then
|
||||
mkdir -p "${target_dir}"
|
||||
for skill in "${managed_skills[@]}"; do
|
||||
apply_skill "${skill}"
|
||||
done
|
||||
fi
|
||||
|
||||
for skill in "${managed_skills[@]}"; do
|
||||
if ! check_skill "${skill}"; then
|
||||
status=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${status} -eq 0 ]]; then
|
||||
echo "[OK] codex skill sync check passed"
|
||||
else
|
||||
echo "[FAIL] codex skill sync check found drift"
|
||||
fi
|
||||
|
||||
exit ${status}
|
||||
323
bin/doctor.php
Executable file
323
bin/doctor.php
Executable file
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MintyPHP\App\AppContainer;
|
||||
use MintyPHP\App\Bootstrap\EnvValidator;
|
||||
use MintyPHP\DB;
|
||||
use MintyPHP\Http\RequestContext;
|
||||
use MintyPHP\Service\Access\AuthorizationService;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\UiAccessService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Auth\AuthService;
|
||||
|
||||
chdir(__DIR__ . '/..');
|
||||
|
||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
|
||||
ini_set('display_errors', '0');
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
require 'config/config.php';
|
||||
require 'lib/Support/helpers.php';
|
||||
|
||||
/** @var AppContainer $container */
|
||||
$container = require 'lib/App/registerContainer.php';
|
||||
setAppContainer($container);
|
||||
RequestContext::start([
|
||||
'channel' => 'cli',
|
||||
'method' => 'CLI',
|
||||
'path' => 'bin/doctor.php',
|
||||
]);
|
||||
$startedAt = microtime(true);
|
||||
|
||||
/** @var list<array{status: string, name: string, message: string}> $results */
|
||||
$results = [];
|
||||
$failCount = 0;
|
||||
$warnCount = 0;
|
||||
|
||||
/**
|
||||
* @param callable():array{status: string, message: string} $check
|
||||
*/
|
||||
$runCheck = static function (string $name, callable $check) use (&$results, &$failCount, &$warnCount): void {
|
||||
try {
|
||||
$result = $check();
|
||||
} catch (Throwable $exception) {
|
||||
$result = [
|
||||
'status' => 'fail',
|
||||
'message' => $exception->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
$status = strtolower(trim((string) ($result['status'] ?? 'fail')));
|
||||
if (!in_array($status, ['ok', 'warn', 'fail'], true)) {
|
||||
$status = 'fail';
|
||||
}
|
||||
|
||||
$message = trim((string) ($result['message'] ?? ''));
|
||||
if ($message === '') {
|
||||
$message = 'no details';
|
||||
}
|
||||
|
||||
if ($status === 'fail') {
|
||||
$failCount++;
|
||||
} elseif ($status === 'warn') {
|
||||
$warnCount++;
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'status' => $status,
|
||||
'name' => $name,
|
||||
'message' => $message,
|
||||
];
|
||||
};
|
||||
|
||||
$runCheck('Environment validation', static function (): array {
|
||||
EnvValidator::validate();
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'all required env keys and formats are valid',
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('App container bootstrap', static function () use ($container): array {
|
||||
if (!$container instanceof AppContainer) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => 'registerContainer.php did not return AppContainer',
|
||||
];
|
||||
}
|
||||
|
||||
app(AuthService::class);
|
||||
app(AuthorizationService::class);
|
||||
app(UiAccessService::class);
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'core services resolved successfully',
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('Database connectivity', static function (): array {
|
||||
$pong = DB::selectValue('select 1');
|
||||
if ((int) $pong !== 1) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => 'select 1 did not return expected value',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'connection established',
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('Database schema basics', static function (): array {
|
||||
$requiredTables = [
|
||||
'users',
|
||||
'roles',
|
||||
'permissions',
|
||||
'user_roles',
|
||||
'role_permissions',
|
||||
'tenants',
|
||||
'departments',
|
||||
'settings',
|
||||
'scheduler_runtime_status',
|
||||
];
|
||||
|
||||
$rows = DB::select(
|
||||
'select table_name from information_schema.tables where table_schema = database() and table_name in (???)',
|
||||
$requiredTables
|
||||
);
|
||||
|
||||
$present = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$table = (string) (($row['tables']['table_name'] ?? $row['table_name'] ?? ''));
|
||||
if ($table !== '') {
|
||||
$present[] = $table;
|
||||
}
|
||||
}
|
||||
$present = array_values(array_unique($present));
|
||||
sort($present, SORT_STRING);
|
||||
|
||||
$missing = array_values(array_diff($requiredTables, $present));
|
||||
if ($missing) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => 'missing tables: ' . implode(', ', $missing),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => sprintf('%d core tables present', count($requiredTables)),
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('Storage path writeability', static function (): array {
|
||||
$storagePath = defined('APP_STORAGE_PATH') && APP_STORAGE_PATH
|
||||
? rtrim((string) APP_STORAGE_PATH, '/')
|
||||
: rtrim(dirname(__DIR__) . '/storage', '/');
|
||||
|
||||
if (!is_dir($storagePath)) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => "storage directory not found: {$storagePath}",
|
||||
];
|
||||
}
|
||||
if (!is_writable($storagePath)) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => "storage directory not writable: {$storagePath}",
|
||||
];
|
||||
}
|
||||
|
||||
$probeFile = $storagePath . '/.doctor-write-probe-' . uniqid('', true);
|
||||
$written = @file_put_contents($probeFile, 'ok');
|
||||
if ($written === false) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => "write probe failed in: {$storagePath}",
|
||||
];
|
||||
}
|
||||
@unlink($probeFile);
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => "storage path is writable ({$storagePath})",
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('RBAC baseline permissions', static function (): array {
|
||||
$requiredPermissions = [
|
||||
PermissionService::USERS_VIEW,
|
||||
PermissionService::TENANTS_VIEW,
|
||||
PermissionService::DEPARTMENTS_VIEW,
|
||||
PermissionService::ROLES_VIEW,
|
||||
PermissionService::PERMISSIONS_VIEW,
|
||||
PermissionService::SETTINGS_VIEW,
|
||||
];
|
||||
|
||||
$rows = DB::select(
|
||||
'select `key` from permissions where active = 1 and `key` in (???)',
|
||||
$requiredPermissions
|
||||
);
|
||||
|
||||
$present = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$key = (string) (($row['permissions']['key'] ?? $row['key'] ?? ''));
|
||||
if ($key !== '') {
|
||||
$present[] = $key;
|
||||
}
|
||||
}
|
||||
$present = array_values(array_unique($present));
|
||||
sort($present, SORT_STRING);
|
||||
|
||||
$missing = array_values(array_diff($requiredPermissions, $present));
|
||||
if ($missing) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => 'missing active permissions: ' . implode(', ', $missing),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => sprintf('%d baseline permissions active', count($requiredPermissions)),
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('Admin role assignment', static function (): array {
|
||||
$count = (int) (DB::selectValue(
|
||||
'select count(distinct ur.user_id) from user_roles ur join roles r on r.id = ur.role_id and r.active = 1 where r.description in (?, ?) or r.id = 1',
|
||||
'Admin',
|
||||
'Administrator'
|
||||
) ?? 0);
|
||||
|
||||
if ($count <= 0) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => 'no active user assigned to Admin/Administrator role',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => sprintf('%d admin user(s) assigned', $count),
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('Scheduler heartbeat', static function (): array {
|
||||
$row = DB::selectOne('select last_heartbeat_at, last_result, last_error_code from scheduler_runtime_status where id = 1 limit 1');
|
||||
$status = is_array($row) ? ($row['scheduler_runtime_status'] ?? $row) : null;
|
||||
if (!is_array($status)) {
|
||||
return [
|
||||
'status' => 'warn',
|
||||
'message' => 'no scheduler runtime status row found yet',
|
||||
];
|
||||
}
|
||||
|
||||
$heartbeat = trim((string) ($status['last_heartbeat_at'] ?? ''));
|
||||
$result = trim((string) ($status['last_result'] ?? 'unknown'));
|
||||
$errorCode = trim((string) ($status['last_error_code'] ?? ''));
|
||||
if ($heartbeat === '') {
|
||||
return [
|
||||
'status' => 'warn',
|
||||
'message' => 'scheduler heartbeat is empty',
|
||||
];
|
||||
}
|
||||
|
||||
$seconds = time() - strtotime($heartbeat . ' UTC');
|
||||
if ($seconds < 0) {
|
||||
$seconds = 0;
|
||||
}
|
||||
|
||||
if ($seconds > 300) {
|
||||
return [
|
||||
'status' => 'warn',
|
||||
'message' => "last heartbeat {$seconds}s ago (result={$result}" . ($errorCode !== '' ? ", error={$errorCode}" : '') . ')',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => "last heartbeat {$seconds}s ago (result={$result}" . ($errorCode !== '' ? ", error={$errorCode}" : '') . ')',
|
||||
];
|
||||
});
|
||||
|
||||
fwrite(STDOUT, "Minty Doctor Report\n");
|
||||
fwrite(STDOUT, "===================\n");
|
||||
foreach ($results as $item) {
|
||||
$statusLabel = strtoupper($item['status']);
|
||||
fwrite(STDOUT, sprintf("[%s] %s: %s\n", $statusLabel, $item['name'], $item['message']));
|
||||
}
|
||||
|
||||
$okCount = count($results) - $warnCount - $failCount;
|
||||
fwrite(STDOUT, sprintf("\nSummary: ok=%d warn=%d fail=%d\n", $okCount, $warnCount, $failCount));
|
||||
|
||||
$exitCode = $failCount > 0 ? 1 : 0;
|
||||
$resultLabel = $failCount > 0 ? 'fail' : ($warnCount > 0 ? 'warn' : 'ok');
|
||||
|
||||
try {
|
||||
app(SystemAuditService::class)->record(
|
||||
'cli.command',
|
||||
$failCount > 0 ? 'failed' : 'success',
|
||||
[
|
||||
'metadata' => [
|
||||
'command' => 'bin/doctor.php',
|
||||
'exit_code' => $exitCode,
|
||||
'duration_ms' => max(0, (int) round((microtime(true) - $startedAt) * 1000)),
|
||||
'result' => $resultLabel,
|
||||
'warn_count' => $warnCount,
|
||||
'fail_count' => $failCount,
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (Throwable) {
|
||||
// fail-open
|
||||
}
|
||||
|
||||
DB::close();
|
||||
exit($exitCode);
|
||||
@@ -8,13 +8,23 @@ chdir(__DIR__ . '/..');
|
||||
require 'vendor/autoload.php';
|
||||
require 'config/config.php';
|
||||
require 'lib/Support/helpers.php';
|
||||
$container = require 'lib/App/registerContainer.php';
|
||||
setAppContainer($container);
|
||||
|
||||
use MintyPHP\DB;
|
||||
use MintyPHP\Http\RequestContext;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Scheduler\SchedulerServicesFactory;
|
||||
|
||||
$exitCode = 0;
|
||||
$startedAt = microtime(true);
|
||||
try {
|
||||
$factory = new SchedulerServicesFactory();
|
||||
RequestContext::start([
|
||||
'channel' => 'scheduler',
|
||||
'method' => 'CLI',
|
||||
'path' => 'bin/scheduler-run.php',
|
||||
]);
|
||||
$factory = app(SchedulerServicesFactory::class);
|
||||
$result = $factory->createSchedulerRunService()->runDueJobs();
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = (string) ($result['error'] ?? 'unexpected_error');
|
||||
@@ -32,6 +42,24 @@ try {
|
||||
fwrite(STDOUT, $line);
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
try {
|
||||
app(SystemAuditService::class)->record('scheduler.run', 'failed', [
|
||||
'error_code' => 'unexpected_error',
|
||||
'metadata' => [
|
||||
'trigger_type' => 'scheduler',
|
||||
'run_status' => 'failed',
|
||||
'processed' => 0,
|
||||
'success_count' => 0,
|
||||
'failed_count' => 0,
|
||||
'skipped_count' => 0,
|
||||
'duration_ms' => max(0, (int) round((microtime(true) - $startedAt) * 1000)),
|
||||
'bootstrap_error' => true,
|
||||
],
|
||||
]);
|
||||
} catch (Throwable) {
|
||||
// fail-open
|
||||
}
|
||||
|
||||
fwrite(STDERR, sprintf("scheduler run failed: unexpected_error: %s\n", $exception->getMessage()));
|
||||
$exitCode = 1;
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user