Public Access
750 lines
27 KiB
PHP
750 lines
27 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: mokoplatform.CLI
|
|
* INGROUP: mokoplatform
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
|
* PATH: /cli/manifest_detect.php
|
|
* VERSION: 09.26.01
|
|
* BRIEF: Auto-detect manifest fields from source files and optionally push to API
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
|
|
use MokoEnterprise\{CliFramework, SourceResolver};
|
|
|
|
class ManifestDetectCli extends CliFramework
|
|
{
|
|
protected function configure(): void
|
|
{
|
|
$this->setDescription('Auto-detect manifest fields from source files');
|
|
$this->addArgument('--path', 'Repository root path', '.');
|
|
$this->addArgument('--json', 'Output as JSON', false);
|
|
$this->addArgument('--diff', 'Show diff against current manifest API values', false);
|
|
$this->addArgument('--update', 'Push detected fields to manifest API', false);
|
|
$this->addArgument('--token', 'Gitea API token (or GITEA_TOKEN env)', '');
|
|
$this->addArgument('--api-base', 'Gitea API base URL', 'https://git.mokoconsulting.tech/api/v1');
|
|
$this->addArgument('--org', 'Gitea org', 'MokoConsulting');
|
|
$this->addArgument('--repo', 'Gitea repo name (auto-detected from remote if empty)', '');
|
|
$this->addArgument('--github-output', 'Append fields to $GITHUB_OUTPUT', false);
|
|
}
|
|
|
|
protected function run(): int
|
|
{
|
|
$path = $this->getArgument('--path');
|
|
$jsonMode = (bool) $this->getArgument('--json');
|
|
$diffMode = (bool) $this->getArgument('--diff');
|
|
$updateMode = (bool) $this->getArgument('--update');
|
|
$ghOutput = (bool) $this->getArgument('--github-output');
|
|
$token = $this->getArgument('--token') ?: getenv('GITEA_TOKEN') ?: '';
|
|
$apiBase = rtrim($this->getArgument('--api-base'), '/');
|
|
$org = $this->getArgument('--org');
|
|
$repoName = $this->getArgument('--repo');
|
|
|
|
$root = realpath($path) ?: $path;
|
|
|
|
if (!is_dir($root)) {
|
|
$this->log('ERROR', "Path does not exist: {$path}");
|
|
return 1;
|
|
}
|
|
|
|
// Auto-detect repo name from git remote
|
|
if ($repoName === '') {
|
|
$repoName = $this->detectRepoName($root);
|
|
}
|
|
|
|
// ── Detect all fields ───────────────────────────────────────
|
|
$detected = $this->detectAll($root, $repoName);
|
|
|
|
// ── Warn about missing fields ────────────────────────────────
|
|
$expected = ['platform', 'name', 'version', 'package_type', 'language', 'entry_point'];
|
|
foreach ($expected as $field) {
|
|
if (!isset($detected[$field]) || $detected[$field] === '') {
|
|
$this->log('WARN', "Could not detect: {$field}");
|
|
}
|
|
}
|
|
|
|
// ── Output ──────────────────────────────────────────────────
|
|
if ($diffMode || $updateMode) {
|
|
if ($token === '') {
|
|
$this->log('ERROR', 'API token required for --diff/--update (use --token or GITEA_TOKEN env)');
|
|
return 1;
|
|
}
|
|
if ($repoName === '') {
|
|
$this->log('ERROR', 'Could not determine repo name (use --repo)');
|
|
return 1;
|
|
}
|
|
|
|
$current = $this->fetchManifest($apiBase, $org, $repoName, $token);
|
|
if ($current === null) {
|
|
$this->log('ERROR', 'Failed to fetch current manifest from API');
|
|
return 1;
|
|
}
|
|
|
|
$changes = $this->computeDiff($current, $detected);
|
|
|
|
if ($diffMode) {
|
|
if (empty($changes)) {
|
|
$this->log('INFO', 'No differences — manifest matches source');
|
|
} else {
|
|
$this->sectionHeader('Manifest Drift');
|
|
foreach ($changes as $field => $info) {
|
|
$this->log('WARN', sprintf(
|
|
'%-20s API: %-30s Detected: %s',
|
|
$field,
|
|
$info['current'] === '' ? '(empty)' : $info['current'],
|
|
$info['detected']
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($updateMode) {
|
|
if (empty($changes)) {
|
|
$this->log('INFO', 'Nothing to update');
|
|
} else {
|
|
$update = array_map(fn($i) => $i['detected'], $changes);
|
|
$ok = $this->pushManifest($apiBase, $org, $repoName, $token, $current, $update);
|
|
if ($ok) {
|
|
$this->log('OK', 'Updated ' . count($update) . ' field(s): ' . implode(', ', array_keys($update)));
|
|
} else {
|
|
$this->log('ERROR', 'Failed to push manifest update');
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
if ($ghOutput) {
|
|
$outputFile = getenv('GITHUB_OUTPUT');
|
|
$lines = [];
|
|
foreach ($detected as $k => $v) {
|
|
$envKey = str_replace('-', '_', $k);
|
|
$lines[] = "{$envKey}={$v}";
|
|
}
|
|
if ($outputFile !== false && $outputFile !== '') {
|
|
file_put_contents($outputFile, implode("\n", $lines) . "\n", FILE_APPEND);
|
|
$this->log('INFO', 'Wrote ' . count($detected) . ' fields to GITHUB_OUTPUT');
|
|
} else {
|
|
$this->log('WARN', 'GITHUB_OUTPUT not set — printing to stdout instead');
|
|
echo implode("\n", $lines) . "\n";
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if ($jsonMode) {
|
|
echo json_encode($detected, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
|
} else {
|
|
foreach ($detected as $k => $v) {
|
|
echo "{$k}={$v}\n";
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// =====================================================================
|
|
// Detection engine
|
|
// =====================================================================
|
|
|
|
private function detectAll(string $root, string $repoName): array
|
|
{
|
|
$platform = $this->detectPlatform($root);
|
|
|
|
$fields = [
|
|
'platform' => $platform,
|
|
'name' => '',
|
|
'description' => '',
|
|
'version' => '',
|
|
'element_name' => '',
|
|
'package_type' => '',
|
|
'language' => '',
|
|
'entry_point' => '',
|
|
'license_spdx' => '',
|
|
'display_name' => '',
|
|
'target_version' => '',
|
|
'php_minimum' => '',
|
|
];
|
|
|
|
switch ($platform) {
|
|
case 'joomla':
|
|
$this->detectJoomla($root, $repoName, $fields);
|
|
break;
|
|
case 'dolibarr':
|
|
$this->detectDolibarr($root, $repoName, $fields);
|
|
break;
|
|
case 'go':
|
|
$this->detectGo($root, $repoName, $fields);
|
|
break;
|
|
case 'mcp':
|
|
$this->detectNode($root, $repoName, $fields);
|
|
break;
|
|
case 'node':
|
|
$this->detectNode($root, $repoName, $fields);
|
|
$fields['platform'] = 'node';
|
|
break;
|
|
default:
|
|
$this->detectGeneric($root, $repoName, $fields);
|
|
break;
|
|
}
|
|
|
|
// Fallbacks
|
|
if ($fields['name'] === '') {
|
|
$fields['name'] = $repoName ?: basename($root);
|
|
}
|
|
if ($fields['entry_point'] === '') {
|
|
$fields['entry_point'] = $this->detectEntryPoint($root);
|
|
}
|
|
if ($fields['license_spdx'] === '') {
|
|
$fields['license_spdx'] = $this->detectLicense($root);
|
|
}
|
|
// description: only from platform-specific source, never guessed
|
|
|
|
// Strip empty values
|
|
return array_filter($fields, fn($v) => $v !== '');
|
|
}
|
|
|
|
// ── Platform detection ──────────────────────────────────────────
|
|
|
|
private function detectPlatform(string $root): string
|
|
{
|
|
// Joomla: look for pkg_*.xml or extension XML in source dirs
|
|
$joomlaXmls = array_merge(
|
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
|
glob("{$root}/pkg_*.xml") ?: []
|
|
);
|
|
if (!empty($joomlaXmls)) {
|
|
return 'joomla';
|
|
}
|
|
|
|
// Check source dirs for any Joomla extension XML
|
|
foreach (SourceResolver::globSource($root, '*.xml') as $xmlFile) {
|
|
$content = file_get_contents($xmlFile);
|
|
if (strpos($content, '<extension') !== false) {
|
|
return 'joomla';
|
|
}
|
|
}
|
|
|
|
// Dolibarr: mod*.class.php with DolibarrModules
|
|
$modFiles = array_merge(
|
|
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
|
glob("{$root}/core/modules/mod*.class.php") ?: []
|
|
);
|
|
foreach ($modFiles as $file) {
|
|
if (strpos(file_get_contents($file), 'DolibarrModules') !== false) {
|
|
return 'dolibarr';
|
|
}
|
|
}
|
|
|
|
// Go
|
|
if (file_exists("{$root}/go.mod")) {
|
|
return 'go';
|
|
}
|
|
|
|
// MCP: package.json with mcp-related content
|
|
if (file_exists("{$root}/package.json")) {
|
|
$pkg = json_decode(file_get_contents("{$root}/package.json"), true) ?? [];
|
|
$deps = array_merge(
|
|
array_keys($pkg['dependencies'] ?? []),
|
|
array_keys($pkg['devDependencies'] ?? [])
|
|
);
|
|
foreach ($deps as $dep) {
|
|
if (strpos($dep, '@modelcontextprotocol/') === 0 || $dep === '@anthropic/mcp-sdk') {
|
|
return 'mcp';
|
|
}
|
|
}
|
|
return 'node';
|
|
}
|
|
|
|
// Python
|
|
if (file_exists("{$root}/pyproject.toml") || file_exists("{$root}/setup.py")) {
|
|
return 'python';
|
|
}
|
|
|
|
return 'generic';
|
|
}
|
|
|
|
// ── Joomla ──────────────────────────────────────────────────────
|
|
|
|
private function detectJoomla(string $root, string $repoName, array &$fields): void
|
|
{
|
|
$fields['language'] = 'PHP';
|
|
|
|
// Find the primary extension manifest XML
|
|
$extManifest = $this->findJoomlaManifest($root);
|
|
if ($extManifest === null) {
|
|
return;
|
|
}
|
|
|
|
$xml = file_get_contents($extManifest);
|
|
|
|
// Type
|
|
$extType = '';
|
|
if (preg_match('/type="([^"]*)"/', $xml, $m)) {
|
|
$extType = $m[1];
|
|
}
|
|
$fields['package_type'] = $extType;
|
|
|
|
// Element name
|
|
$element = '';
|
|
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $m)) {
|
|
$element = $m[1];
|
|
}
|
|
if ($element === '' && preg_match('/module="([^"]*)"/', $xml, $m)) {
|
|
$element = $m[1];
|
|
}
|
|
if ($element === '' && preg_match('/plugin="([^"]*)"/', $xml, $m)) {
|
|
$element = $m[1];
|
|
}
|
|
if ($extType === 'package' && preg_match('/<packagename>([^<]+)<\/packagename>/', $xml, $m)) {
|
|
$element = $m[1];
|
|
}
|
|
if ($element === '') {
|
|
$element = strtolower(basename($extManifest, '.xml'));
|
|
}
|
|
|
|
// Ensure element has type prefix (API stores full element_name like pkg_mokosuite)
|
|
$prefixMap = [
|
|
'package' => 'pkg_', 'component' => 'com_', 'module' => 'mod_',
|
|
'template' => 'tpl_', 'library' => 'lib_', 'file' => 'file_',
|
|
];
|
|
if (isset($prefixMap[$extType])) {
|
|
$prefix = $prefixMap[$extType];
|
|
// Only add prefix if not already present (check all known prefixes)
|
|
$hasPrefix = false;
|
|
foreach ($prefixMap as $p) {
|
|
if (strpos($element, $p) === 0) { $hasPrefix = true; break; }
|
|
}
|
|
if (strpos($element, 'plg_') === 0) { $hasPrefix = true; }
|
|
if (!$hasPrefix) {
|
|
$element = $prefix . $element;
|
|
}
|
|
} elseif ($extType === 'plugin') {
|
|
$folder = '';
|
|
if (preg_match('/group="([^"]*)"/', $xml, $gm)) {
|
|
$folder = $gm[1];
|
|
}
|
|
if ($folder !== '' && strpos($element, 'plg_') !== 0) {
|
|
$element = "plg_{$folder}_" . $element;
|
|
}
|
|
}
|
|
$fields['element_name'] = $element;
|
|
|
|
// Name
|
|
if (preg_match('/<name>([^<]+)<\/name>/', $xml, $m)) {
|
|
$fields['name'] = trim($m[1]);
|
|
}
|
|
|
|
// Version
|
|
if (preg_match('/<version>([^<]+)<\/version>/', $xml, $m)) {
|
|
$fields['version'] = trim($m[1]);
|
|
}
|
|
|
|
// Description
|
|
if (preg_match('/<description>([^<]+)<\/description>/', $xml, $m)) {
|
|
$desc = trim($m[1]);
|
|
// Skip language string keys like COM_MOKOSUITE_DESCRIPTION
|
|
if (strpos($desc, '_') === false || strlen($desc) > 60) {
|
|
$fields['description'] = $desc;
|
|
}
|
|
}
|
|
|
|
// Display name for update feeds
|
|
if (!empty($fields['name'])) {
|
|
$name = $fields['name'];
|
|
// If name already has "Type - " prefix, use as-is
|
|
if (preg_match('/^(Package|Component|Module|Plugin|Template|Library)\s*-\s*/i', $name)) {
|
|
$fields['display_name'] = $name;
|
|
} elseif (!empty($extType)) {
|
|
$fields['display_name'] = ucfirst($extType) . ' - ' . $name;
|
|
}
|
|
}
|
|
|
|
// Target Joomla version
|
|
if (preg_match('/<targetplatform\s[^>]*version="([^"]+)"/', $xml, $m)) {
|
|
$fields['target_version'] = trim($m[1]);
|
|
} else {
|
|
// Default for Joomla 5/6
|
|
$fields['target_version'] = '(5|6)\..*';
|
|
}
|
|
|
|
// PHP minimum
|
|
if (preg_match('/<php_minimum>([^<]+)<\/php_minimum>/', $xml, $m)) {
|
|
$fields['php_minimum'] = trim($m[1]);
|
|
}
|
|
|
|
// License
|
|
if (preg_match('/<license>([^<]+)<\/license>/', $xml, $m)) {
|
|
$fields['license_spdx'] = $this->normalizeLicense(trim($m[1]));
|
|
}
|
|
}
|
|
|
|
private function findJoomlaManifest(string $root): ?string
|
|
{
|
|
// Priority: pkg_*.xml (package manifest)
|
|
$pkgXmls = array_merge(
|
|
SourceResolver::globSource($root, 'pkg_*.xml'),
|
|
glob("{$root}/pkg_*.xml") ?: []
|
|
);
|
|
if (!empty($pkgXmls)) {
|
|
return $pkgXmls[0];
|
|
}
|
|
|
|
// Any extension XML in source dir
|
|
foreach (SourceResolver::globSource($root, '*.xml') as $file) {
|
|
$content = file_get_contents($file);
|
|
if (strpos($content, '<extension') !== false) {
|
|
return $file;
|
|
}
|
|
}
|
|
|
|
// Root level
|
|
foreach (glob("{$root}/*.xml") ?: [] as $file) {
|
|
$content = file_get_contents($file);
|
|
if (strpos($content, '<extension') !== false) {
|
|
return $file;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ── Dolibarr ────────────────────────────────────────────────────
|
|
|
|
private function detectDolibarr(string $root, string $repoName, array &$fields): void
|
|
{
|
|
$fields['language'] = 'PHP';
|
|
$fields['package_type'] = 'dolibarr-module';
|
|
|
|
$modFile = $this->findDolibarrModule($root);
|
|
if ($modFile === null) {
|
|
return;
|
|
}
|
|
|
|
$content = file_get_contents($modFile);
|
|
|
|
// Element name from class file
|
|
$modBasename = basename($modFile, '.class.php');
|
|
$fields['element_name'] = strtolower(preg_replace('/^mod/', '', $modBasename));
|
|
|
|
// Name
|
|
if (preg_match('/\$this->name\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
|
$fields['name'] = $m[1];
|
|
}
|
|
|
|
// Version
|
|
if (preg_match('/\$this->version\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
|
$fields['version'] = $m[1];
|
|
}
|
|
|
|
// Description
|
|
if (preg_match('/\$this->description\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
|
|
$desc = $m[1];
|
|
if (strpos($desc, '$') === false) {
|
|
$fields['description'] = $desc;
|
|
}
|
|
}
|
|
|
|
// License
|
|
if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) {
|
|
$fields['license_spdx'] = $m[1];
|
|
}
|
|
}
|
|
|
|
private function findDolibarrModule(string $root): ?string
|
|
{
|
|
$candidates = array_merge(
|
|
SourceResolver::globSource($root, 'core/modules/mod*.class.php'),
|
|
glob("{$root}/core/modules/mod*.class.php") ?: []
|
|
);
|
|
foreach ($candidates as $file) {
|
|
if (strpos(file_get_contents($file), 'DolibarrModules') !== false) {
|
|
return $file;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ── Go ──────────────────────────────────────────────────────────
|
|
|
|
private function detectGo(string $root, string $repoName, array &$fields): void
|
|
{
|
|
$fields['language'] = 'Go';
|
|
$fields['package_type'] = 'application';
|
|
$fields['entry_point'] = './';
|
|
|
|
$goMod = "{$root}/go.mod";
|
|
if (!file_exists($goMod)) {
|
|
return;
|
|
}
|
|
|
|
$content = file_get_contents($goMod);
|
|
|
|
// Module path → name
|
|
if (preg_match('/^module\s+(\S+)/m', $content, $m)) {
|
|
$modulePath = $m[1];
|
|
$parts = explode('/', $modulePath);
|
|
$fields['name'] = end($parts);
|
|
}
|
|
|
|
// Go version
|
|
if (preg_match('/^go\s+(\S+)/m', $content, $m)) {
|
|
// This is Go language version, not the project version
|
|
// Project version comes from git tags or source files
|
|
}
|
|
|
|
// License
|
|
$fields['license_spdx'] = $this->detectLicense($root);
|
|
}
|
|
|
|
// ── Node / MCP ──────────────────────────────────────────────────
|
|
|
|
private function detectNode(string $root, string $repoName, array &$fields): void
|
|
{
|
|
$pkgFile = "{$root}/package.json";
|
|
if (!file_exists($pkgFile)) {
|
|
return;
|
|
}
|
|
|
|
$pkg = json_decode(file_get_contents($pkgFile), true) ?? [];
|
|
|
|
$fields['name'] = $pkg['name'] ?? '';
|
|
// Strip npm scope
|
|
if (strpos($fields['name'], '/') !== false) {
|
|
$fields['name'] = explode('/', $fields['name'])[1];
|
|
}
|
|
|
|
$fields['version'] = $pkg['version'] ?? '';
|
|
$fields['description'] = $pkg['description'] ?? '';
|
|
$fields['license_spdx'] = $pkg['license'] ?? '';
|
|
|
|
// Language detection
|
|
if (file_exists("{$root}/tsconfig.json")) {
|
|
$fields['language'] = 'TypeScript';
|
|
} else {
|
|
$fields['language'] = 'JavaScript';
|
|
}
|
|
|
|
// Package type
|
|
$deps = array_merge(
|
|
array_keys($pkg['dependencies'] ?? []),
|
|
array_keys($pkg['devDependencies'] ?? [])
|
|
);
|
|
$isMcp = false;
|
|
foreach ($deps as $dep) {
|
|
if (strpos($dep, '@modelcontextprotocol/') === 0 || $dep === '@anthropic/mcp-sdk') {
|
|
$isMcp = true;
|
|
break;
|
|
}
|
|
}
|
|
$fields['package_type'] = $isMcp ? 'mcp-server' : 'application';
|
|
|
|
// Entry point
|
|
if (file_exists("{$root}/dist")) {
|
|
$fields['entry_point'] = 'dist/';
|
|
} elseif (file_exists("{$root}/src")) {
|
|
$fields['entry_point'] = 'src/';
|
|
} else {
|
|
$fields['entry_point'] = './';
|
|
}
|
|
}
|
|
|
|
// ── Generic ─────────────────────────────────────────────────────
|
|
|
|
private function detectGeneric(string $root, string $repoName, array &$fields): void
|
|
{
|
|
$fields['package_type'] = 'generic';
|
|
|
|
// Try to detect language from file extensions
|
|
$fields['language'] = $this->detectLanguageFromFiles($root);
|
|
$fields['license_spdx'] = $this->detectLicense($root);
|
|
}
|
|
|
|
// =====================================================================
|
|
// Shared detection helpers
|
|
// =====================================================================
|
|
|
|
private function detectEntryPoint(string $root): string
|
|
{
|
|
$abs = SourceResolver::resolveAbsolute($root);
|
|
if ($abs !== null) {
|
|
return basename($abs) . '/';
|
|
}
|
|
if (is_dir("{$root}/dist")) return 'dist/';
|
|
if (is_dir("{$root}/src")) return 'src/';
|
|
return './';
|
|
}
|
|
|
|
private function detectLicense(string $root): string
|
|
{
|
|
// Check LICENSE file
|
|
foreach (['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING'] as $name) {
|
|
$file = "{$root}/{$name}";
|
|
if (!file_exists($file)) continue;
|
|
$content = file_get_contents($file);
|
|
|
|
// SPDX header
|
|
if (preg_match('/SPDX-License-Identifier:\s*(\S+)/', $content, $m)) {
|
|
return $m[1];
|
|
}
|
|
|
|
// Common license patterns
|
|
if (strpos($content, 'GNU GENERAL PUBLIC LICENSE') !== false) {
|
|
if (strpos($content, 'Version 3') !== false) return 'GPL-3.0-or-later';
|
|
if (strpos($content, 'Version 2') !== false) return 'GPL-2.0-or-later';
|
|
}
|
|
if (strpos($content, 'MIT License') !== false) return 'MIT';
|
|
if (strpos($content, 'Apache License') !== false && strpos($content, 'Version 2.0') !== false) return 'Apache-2.0';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
|
|
private function detectLanguageFromFiles(string $root): string
|
|
{
|
|
$counts = ['PHP' => 0, 'Go' => 0, 'TypeScript' => 0, 'JavaScript' => 0, 'Python' => 0, 'Shell' => 0];
|
|
|
|
$extensions = [
|
|
'php' => 'PHP', 'go' => 'Go', 'ts' => 'TypeScript',
|
|
'js' => 'JavaScript', 'py' => 'Python', 'sh' => 'Shell',
|
|
];
|
|
|
|
// Quick scan: only check top two levels
|
|
foreach (glob("{$root}/*") ?: [] as $item) {
|
|
$ext = pathinfo($item, PATHINFO_EXTENSION);
|
|
if (isset($extensions[$ext])) {
|
|
$counts[$extensions[$ext]]++;
|
|
}
|
|
if (is_dir($item) && basename($item)[0] !== '.') {
|
|
foreach (glob("{$item}/*") ?: [] as $subItem) {
|
|
$ext = pathinfo($subItem, PATHINFO_EXTENSION);
|
|
if (isset($extensions[$ext])) {
|
|
$counts[$extensions[$ext]]++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
arsort($counts);
|
|
$top = key($counts);
|
|
return $counts[$top] > 0 ? $top : '';
|
|
}
|
|
|
|
private function normalizeLicense(string $license): string
|
|
{
|
|
$lower = strtolower($license);
|
|
$isGpl = strpos($lower, 'gpl') !== false || strpos($lower, 'general public license') !== false;
|
|
if ($isGpl && strpos($lower, '3') !== false) return 'GPL-3.0-or-later';
|
|
if ($isGpl && strpos($lower, '2') !== false) return 'GPL-2.0-or-later';
|
|
if ($lower === 'mit' || strpos($lower, 'mit license') !== false) return 'MIT';
|
|
if (strpos($lower, 'apache') !== false) return 'Apache-2.0';
|
|
return $license;
|
|
}
|
|
|
|
private function detectRepoName(string $root): string
|
|
{
|
|
$gitConfig = "{$root}/.git/config";
|
|
if (!file_exists($gitConfig)) {
|
|
return basename($root);
|
|
}
|
|
|
|
$content = file_get_contents($gitConfig);
|
|
if (preg_match('/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?\s*$/m', $content, $m)) {
|
|
return $m[1];
|
|
}
|
|
|
|
return basename($root);
|
|
}
|
|
|
|
// =====================================================================
|
|
// API interaction
|
|
// =====================================================================
|
|
|
|
private function fetchManifest(string $apiBase, string $org, string $repo, string $token): ?array
|
|
{
|
|
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
|
$ctx = stream_context_create([
|
|
'http' => [
|
|
'header' => "Authorization: token {$token}\r\nAccept: application/json\r\n",
|
|
'timeout' => 10,
|
|
],
|
|
]);
|
|
|
|
$body = @file_get_contents($url, false, $ctx);
|
|
if ($body === false) return null;
|
|
|
|
return json_decode($body, true);
|
|
}
|
|
|
|
private function computeDiff(array $current, array $detected): array
|
|
{
|
|
// Map detected keys to API keys (underscores match)
|
|
$changes = [];
|
|
|
|
foreach ($detected as $key => $value) {
|
|
$apiKey = $key;
|
|
$currentVal = $current[$apiKey] ?? '';
|
|
|
|
// Only flag as changed if detected value is non-empty and differs
|
|
if ($value !== '' && $value !== $currentVal) {
|
|
// Don't overwrite a non-empty API value with a detected value
|
|
// unless the API value is actually empty
|
|
if ($currentVal === '' || $this->shouldOverride($key, $currentVal, $value)) {
|
|
$changes[$key] = [
|
|
'current' => $currentVal,
|
|
'detected' => $value,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $changes;
|
|
}
|
|
|
|
private function shouldOverride(string $field, string $current, string $detected): bool
|
|
{
|
|
// Version: detected from source is authoritative
|
|
if ($field === 'version') return true;
|
|
|
|
// These fields: source files are authoritative
|
|
if (in_array($field, ['element_name', 'package_type', 'language', 'entry_point'], true)) {
|
|
return true;
|
|
}
|
|
|
|
// For other fields, only fill empty — don't overwrite manual edits
|
|
return false;
|
|
}
|
|
|
|
private function pushManifest(string $apiBase, string $org, string $repo, string $token, array $current, array $update): bool
|
|
{
|
|
$merged = array_merge($current, $update);
|
|
$url = "{$apiBase}/repos/{$org}/{$repo}/manifest";
|
|
$payload = json_encode($merged);
|
|
|
|
$ctx = stream_context_create([
|
|
'http' => [
|
|
'method' => 'PUT',
|
|
'header' => "Authorization: token {$token}\r\nContent-Type: application/json\r\nAccept: application/json\r\n",
|
|
'content' => $payload,
|
|
'timeout' => 10,
|
|
],
|
|
]);
|
|
|
|
$body = @file_get_contents($url, false, $ctx);
|
|
return $body !== false;
|
|
}
|
|
}
|
|
|
|
$app = new ManifestDetectCli();
|
|
exit($app->execute());
|