Public Access
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 252e5ef3fb |
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 09.37.00
|
||||
# VERSION: 09.34.01
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
+5
-5
@@ -12,12 +12,12 @@ BRIEF: Release changelog
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
## [09.37.00] --- 2026-06-21
|
||||
## [09.34.00] --- 2026-06-21
|
||||
|
||||
## [09.36.00] --- 2026-06-21
|
||||
## [09.33.00] --- 2026-06-21
|
||||
|
||||
## [09.36.00] --- 2026-06-21
|
||||
## [09.33.00] --- 2026-06-21
|
||||
|
||||
## [09.35.00] --- 2026-06-21
|
||||
## [09.32.00] --- 2026-06-21
|
||||
|
||||
## [09.35.00] --- 2026-06-21
|
||||
## [09.32.00] --- 2026-06-21
|
||||
|
||||
@@ -6,7 +6,7 @@ DEFGROUP: MokoPlatform.Root
|
||||
INGROUP: MokoPlatform
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
PATH: /README.md
|
||||
VERSION: 09.37.00
|
||||
VERSION: 09.34.01
|
||||
BRIEF: Project overview and documentation
|
||||
-->
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* INGROUP: MokoPlatform.Scripts
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
* PATH: /automation/update_dependencies.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Cross-repo dependency update automation — scan, update, PR, auto-merge
|
||||
*/
|
||||
|
||||
|
||||
@@ -178,7 +178,6 @@ const COMMAND_MAP = [
|
||||
'repo:archive' => 'cli/archive_repo.php',
|
||||
'repo:scaffold-client' => 'cli/scaffold_client.php',
|
||||
'repo:provision' => 'cli/client_provision.php',
|
||||
'repo:wizard' => 'cli/repo_wizard.php',
|
||||
'repo:rename-branch' => 'cli/branch_rename.php',
|
||||
'repo:reset-dev' => 'cli/dev_branch_reset.php',
|
||||
|
||||
@@ -200,7 +199,6 @@ const COMMAND_MAP = [
|
||||
'deploy:sftp' => 'deploy/deploy-sftp.php',
|
||||
'deploy:backup' => 'deploy/backup-before-deploy.php',
|
||||
'deploy:health-check' => 'deploy/health-check.php',
|
||||
'deploy:verify' => 'deploy/deploy-and-verify.php',
|
||||
'deploy:rollback' => 'deploy/rollback-joomla.php',
|
||||
'deploy:sync' => 'deploy/sync-joomla.php',
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/branch_rename.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old)
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/bulk_workflow_push.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Push a workflow file to all governed repos via the Gitea Contents API
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/bulk_workflow_trigger.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Trigger a workflow across multiple repos at once
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/client_dashboard.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Generate unified client dashboard HTML
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/client_inventory.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Discover and list all client-waas repos with their server configuration status
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/client_provision.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Provision a new client environment end-to-end
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/grafana_dashboard.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Manage Grafana dashboards via API
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/joomla_build.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported
|
||||
* NOTE: Called by pre-release and auto-release workflows.
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/joomla_metadata_validate.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Validate MokoGitea repo metadata against Joomla extension manifest XML
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/manifest_detect.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Auto-detect manifest fields from source files and optionally push to API
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/manifest_integrity.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Cross-check manifest API fields against repo contents across the org
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/manifest_licensing.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokocli
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
* PATH: /cli/manifest_read.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Read repo metadata from Gitea manifest API, auto-detect the rest
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/platform_detect.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Auto-detect repository platform type and optionally update manifest
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokocli
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
* PATH: /cli/release_cascade.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Cascade release zip to all lower stability channels
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/release_publish.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Publish a release and create copies for all lesser stability streams.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,429 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: mokocli.CLI
|
||||
* INGROUP: mokocli
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
* PATH: /cli/repo_wizard.php
|
||||
* BRIEF: Interactive configuration wizard for new repositories
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoCli\{CliFramework, Config, PlatformAdapterFactory};
|
||||
|
||||
/**
|
||||
* Interactive repo setup wizard.
|
||||
*
|
||||
* Walks through platform selection, generates config files, workflows,
|
||||
* and optionally creates the repo on Gitea via API.
|
||||
*
|
||||
* @see https://git.mokoconsulting.tech/MokoConsulting/mokocli/issues/145
|
||||
*/
|
||||
class RepoWizard extends CliFramework
|
||||
{
|
||||
private const PLATFORMS = [
|
||||
'joomla' => 'Joomla extension (component, module, plugin, package)',
|
||||
'dolibarr' => 'Dolibarr ERP module',
|
||||
'nodejs' => 'Node.js / TypeScript project',
|
||||
'python' => 'Python project',
|
||||
'mcp-server' => 'MCP server (Model Context Protocol)',
|
||||
'generic' => 'Generic PHP or multi-language project',
|
||||
];
|
||||
|
||||
private const LICENSES = [
|
||||
'GPL-3.0-or-later' => 'GNU General Public License v3',
|
||||
'MIT' => 'MIT License',
|
||||
'Apache-2.0' => 'Apache License 2.0',
|
||||
'proprietary' => 'Proprietary / All rights reserved',
|
||||
];
|
||||
|
||||
/** Collected wizard answers. */
|
||||
private array $answers = [];
|
||||
|
||||
/** When true, skip all interactive prompts and use defaults. */
|
||||
private bool $nonInteractive = false;
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('Interactive configuration wizard for new repositories');
|
||||
$this->addArgument('--path', 'Directory to generate files in', '.');
|
||||
$this->addArgument('--create-remote', 'Create repo on Gitea via API', false);
|
||||
$this->addArgument('--non-interactive', 'Use defaults (no prompts)', false);
|
||||
}
|
||||
|
||||
protected function run(): int
|
||||
{
|
||||
$rawPath = $this->getArgument('--path', '.');
|
||||
$targetPath = realpath($rawPath) ?: $rawPath;
|
||||
$this->nonInteractive = (bool) $this->getArgument('--non-interactive', false);
|
||||
|
||||
// Validate target path
|
||||
if (!is_dir($targetPath) && !@mkdir($targetPath, 0755, true)) {
|
||||
$this->log('ERROR', "Target path does not exist and cannot be created: {$targetPath}");
|
||||
return self::EXIT_USAGE;
|
||||
}
|
||||
$targetPath = realpath($targetPath) ?: $targetPath;
|
||||
|
||||
$this->section('MokoCli Repository Wizard');
|
||||
|
||||
// ── Gather info ──────────────────────────────────────────────
|
||||
$this->answers['name'] = $this->ask('Repository name', basename($targetPath));
|
||||
$this->answers['platform'] = $this->choose('Platform type', self::PLATFORMS, 'generic');
|
||||
$this->answers['org'] = $this->ask('Organization', 'MokoConsulting');
|
||||
$this->answers['description'] = $this->ask('Description', '');
|
||||
$this->answers['license'] = $this->choose('License', self::LICENSES, 'GPL-3.0-or-later');
|
||||
|
||||
// ── Confirm ──────────────────────────────────────────────────
|
||||
$this->section('Configuration Summary');
|
||||
foreach ($this->answers as $key => $value) {
|
||||
$this->log('INFO', sprintf(' %-12s %s', $key . ':', $value));
|
||||
}
|
||||
|
||||
if (!$this->confirm('Proceed with these settings?', true)) {
|
||||
$this->log('INFO', 'Wizard cancelled');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── Generate files ───────────────────────────────────────────
|
||||
$this->section('Generating files');
|
||||
$generated = $this->generateFiles($targetPath);
|
||||
|
||||
foreach ($generated as $file) {
|
||||
$this->status(true, $file);
|
||||
}
|
||||
|
||||
// ── Create remote repo ───────────────────────────────────────
|
||||
if ($this->getArgument('--create-remote', false)) {
|
||||
$this->section('Creating remote repository');
|
||||
$this->createRemoteRepo();
|
||||
}
|
||||
|
||||
$this->log('INFO', '');
|
||||
$this->log('INFO', 'Generated ' . count($generated) . " files in {$targetPath}");
|
||||
$this->log('INFO', 'Next: git init && git add -A && git commit -m "chore: initial scaffold"');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── File generation ──────────────────────────────────────────────
|
||||
|
||||
private function generateFiles(string $path): array
|
||||
{
|
||||
$platform = $this->answers['platform'];
|
||||
$name = $this->answers['name'];
|
||||
$generated = [];
|
||||
|
||||
// .editorconfig
|
||||
$generated[] = $this->writeFile($path, '.editorconfig', $this->editorconfig());
|
||||
|
||||
// README.md
|
||||
$generated[] = $this->writeFile($path, 'README.md', $this->readme());
|
||||
|
||||
// CHANGELOG.md
|
||||
$generated[] = $this->writeFile($path, 'CHANGELOG.md', $this->changelog());
|
||||
|
||||
// LICENSE
|
||||
if ($this->answers['license'] !== 'proprietary') {
|
||||
$generated[] = $this->writeFile($path, 'LICENSE', "See SPDX: {$this->answers['license']}");
|
||||
}
|
||||
|
||||
// Platform-specific configs
|
||||
switch ($platform) {
|
||||
case 'joomla':
|
||||
case 'dolibarr':
|
||||
case 'generic':
|
||||
$generated[] = $this->writeFile($path, 'phpcs.xml', $this->phpcsXml());
|
||||
$generated[] = $this->writeFile($path, 'phpstan.neon', $this->phpstanNeon());
|
||||
$generated[] = $this->writeFile($path, 'composer.json', $this->composerJson());
|
||||
break;
|
||||
case 'nodejs':
|
||||
case 'mcp-server':
|
||||
$generated[] = $this->writeFile($path, 'package.json', $this->packageJson());
|
||||
$generated[] = $this->writeFile($path, 'tsconfig.json', $this->tsconfigJson());
|
||||
$generated[] = $this->writeFile($path, '.eslintrc.json', $this->eslintrc());
|
||||
break;
|
||||
case 'python':
|
||||
$generated[] = $this->writeFile($path, 'pyproject.toml', $this->pyprojectToml());
|
||||
$generated[] = $this->writeFile($path, 'requirements.txt', '');
|
||||
break;
|
||||
}
|
||||
|
||||
// .mokogitea/workflows
|
||||
$generated[] = $this->writeFile($path, '.mokogitea/workflows/pr-check.yml',
|
||||
"# PR check workflow — synced from mokocli templates\n# Run: moko sync to update\n");
|
||||
|
||||
// .gitignore
|
||||
$generated[] = $this->writeFile($path, '.gitignore', $this->gitignore($platform));
|
||||
|
||||
// Source directory
|
||||
$srcDir = in_array($platform, ['joomla', 'dolibarr', 'generic']) ? 'source' : 'src';
|
||||
if (!is_dir("{$path}/{$srcDir}")) {
|
||||
@mkdir("{$path}/{$srcDir}", 0755, true);
|
||||
$generated[] = "{$srcDir}/";
|
||||
}
|
||||
|
||||
return array_filter($generated);
|
||||
}
|
||||
|
||||
private function writeFile(string $basePath, string $relativePath, string $content): ?string
|
||||
{
|
||||
$fullPath = $basePath . '/' . $relativePath;
|
||||
$dir = dirname($fullPath);
|
||||
|
||||
if (file_exists($fullPath)) {
|
||||
$this->log('DEBUG', " SKIP {$relativePath} (already exists)");
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dryRun) {
|
||||
$this->log('INFO', "[dry-run] Would create {$relativePath}");
|
||||
return $relativePath;
|
||||
}
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($fullPath, $content);
|
||||
return $relativePath;
|
||||
}
|
||||
|
||||
// ── Remote repo creation ─────────────────────────────────────────
|
||||
|
||||
private function createRemoteRepo(): void
|
||||
{
|
||||
try {
|
||||
$config = Config::load();
|
||||
$adapter = PlatformAdapterFactory::create($config);
|
||||
$org = $this->answers['org'];
|
||||
|
||||
if ($this->dryRun) {
|
||||
$this->log('INFO', "[dry-run] Would create {$org}/{$this->answers['name']} on Gitea");
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $adapter->createRepository($org, $this->answers['name'], [
|
||||
'description' => $this->answers['description'],
|
||||
'private' => false,
|
||||
]);
|
||||
|
||||
$url = $result['html_url'] ?? "{$org}/{$this->answers['name']}";
|
||||
$this->log('INFO', "Created: {$url}");
|
||||
} catch (\Exception $e) {
|
||||
$this->log('ERROR', "Failed to create remote repo: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Interactive helpers (respect --non-interactive) ─────────────
|
||||
|
||||
private function ask(string $prompt, string $default): string
|
||||
{
|
||||
if ($this->nonInteractive) {
|
||||
return $default;
|
||||
}
|
||||
return $this->input($prompt, $default);
|
||||
}
|
||||
|
||||
private function choose(string $prompt, array $options, string $default): string
|
||||
{
|
||||
if ($this->nonInteractive) {
|
||||
return $default;
|
||||
}
|
||||
$keys = array_keys($options);
|
||||
$labels = [];
|
||||
foreach ($options as $key => $desc) {
|
||||
$labels[] = "{$key} — {$desc}";
|
||||
}
|
||||
$chosen = $this->select($prompt, $labels);
|
||||
// Extract the key from "key — description"
|
||||
$chosenKey = explode(' — ', $chosen, 2)[0] ?? $default;
|
||||
return in_array($chosenKey, $keys, true) ? $chosenKey : $default;
|
||||
}
|
||||
|
||||
// ── Template content ─────────────────────────────────────────────
|
||||
|
||||
private function editorconfig(): string
|
||||
{
|
||||
return <<<'CONF'
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
CONF;
|
||||
}
|
||||
|
||||
private function readme(): string
|
||||
{
|
||||
$name = $this->answers['name'];
|
||||
$desc = $this->answers['description'] ?: 'A Moko Consulting project.';
|
||||
$license = $this->answers['license'];
|
||||
|
||||
return <<<MD
|
||||
# {$name}
|
||||
|
||||
{$desc}
|
||||
|
||||
## License
|
||||
|
||||
{$license}
|
||||
MD;
|
||||
}
|
||||
|
||||
private function changelog(): string
|
||||
{
|
||||
return <<<MD
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Initial project scaffold
|
||||
MD;
|
||||
}
|
||||
|
||||
private function composerJson(): string
|
||||
{
|
||||
$data = [
|
||||
'name' => 'mokoconsulting/' . strtolower($this->answers['name']),
|
||||
'description' => $this->answers['description'] ?: $this->answers['name'],
|
||||
'type' => 'library',
|
||||
'license' => $this->answers['license'],
|
||||
'require' => ['php' => '>=8.1'],
|
||||
'autoload' => ['psr-4' => new \stdClass()],
|
||||
];
|
||||
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
}
|
||||
|
||||
private function phpcsXml(): string
|
||||
{
|
||||
return <<<'XML'
|
||||
<?xml version="1.0"?>
|
||||
<ruleset name="MokoCli">
|
||||
<rule ref="PSR12"/>
|
||||
<file>source/</file>
|
||||
<exclude-pattern>vendor/*</exclude-pattern>
|
||||
</ruleset>
|
||||
XML;
|
||||
}
|
||||
|
||||
private function phpstanNeon(): string
|
||||
{
|
||||
return <<<'NEON'
|
||||
parameters:
|
||||
level: 6
|
||||
paths:
|
||||
- source/
|
||||
NEON;
|
||||
}
|
||||
|
||||
private function packageJson(): string
|
||||
{
|
||||
$data = [
|
||||
'name' => '@mokoconsulting/' . strtolower($this->answers['name']),
|
||||
'version' => '0.1.0',
|
||||
'description' => $this->answers['description'] ?: $this->answers['name'],
|
||||
'type' => 'module',
|
||||
'scripts' => ['build' => 'tsc', 'start' => 'node dist/index.js'],
|
||||
'devDependencies' => ['typescript' => '^5.0'],
|
||||
];
|
||||
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
}
|
||||
|
||||
private function tsconfigJson(): string
|
||||
{
|
||||
return <<<'JSON'
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
JSON;
|
||||
}
|
||||
|
||||
private function eslintrc(): string
|
||||
{
|
||||
return <<<'JSON'
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"]
|
||||
}
|
||||
JSON;
|
||||
}
|
||||
|
||||
private function pyprojectToml(): string
|
||||
{
|
||||
$name = strtolower($this->answers['name']);
|
||||
$desc = str_replace(['\\', '"'], ['\\\\', '\\"'], $this->answers['description'] ?: $this->answers['name']);
|
||||
|
||||
return <<<TOML
|
||||
[project]
|
||||
name = "{$name}"
|
||||
version = "0.1.0"
|
||||
description = "{$desc}"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=68.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
TOML;
|
||||
}
|
||||
|
||||
private function gitignore(string $platform): string
|
||||
{
|
||||
$common = <<<'GI'
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
GI;
|
||||
|
||||
$extra = match ($platform) {
|
||||
'joomla', 'dolibarr', 'generic' => "\n# PHP\nvendor/\n.phpunit.result.cache\n",
|
||||
'nodejs', 'mcp-server' => "\n# Node\nnode_modules/\ndist/\n*.tsbuildinfo\n",
|
||||
'python' => "\n# Python\n__pycache__/\n*.pyc\n.venv/\n*.egg-info/\n",
|
||||
default => '',
|
||||
};
|
||||
|
||||
return $common . $extra;
|
||||
}
|
||||
}
|
||||
|
||||
$app = new RepoWizard('repo_wizard');
|
||||
exit($app->execute());
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/scaffold_client.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/updates_xml_sync.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Sync updates.xml to target branches via Gitea API
|
||||
* NOTE: Called by pre-release and auto-release workflows after updates.xml
|
||||
* is modified on the current branch. Pushes the file to other branches
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/version_auto_bump.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash
|
||||
*/
|
||||
|
||||
|
||||
@@ -370,7 +370,7 @@ class VersionBumpCli extends CliFramework
|
||||
/**
|
||||
* Scan git release tags for the highest version across all channels.
|
||||
*
|
||||
* Checks release names like "MokoSuiteClient (VERSION: 09.37.00)" in
|
||||
* Checks release names like "MokoSuiteClient (VERSION: 09.34.01)" in
|
||||
* git tags (stable, release-candidate, development, etc.) to find the
|
||||
* highest version that has been released on any channel.
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/version_check.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Validate version consistency across README, manifests, and sub-packages
|
||||
*/
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
* INGROUP: mokoplatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /cli/wiki_sync.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Sync select wiki pages from mokoplatform to all template repos
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: moko-platform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /cli/workflow_sync.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Sync workflows from Generic → platform templates → live repos based on manifest.platform
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /deploy/backup-before-deploy.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Snapshot Joomla directories before deployment for rollback capability
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
#!/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.Scripts.Deploy
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
* PATH: /deploy/deploy-and-verify.php
|
||||
* BRIEF: Deploy with automatic health check and rollback on failure
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
||||
|
||||
use MokoCli\{AuditLogger, CliFramework};
|
||||
|
||||
/**
|
||||
* Deploy-and-Verify: orchestrates backup → deploy → health-check → rollback.
|
||||
*
|
||||
* If the health check fails after deployment, automatically triggers a rollback
|
||||
* using the pre-deploy snapshot, with full audit trail.
|
||||
*
|
||||
* @see https://git.mokoconsulting.tech/MokoConsulting/mokocli/issues/147
|
||||
*/
|
||||
class DeployAndVerify extends CliFramework
|
||||
{
|
||||
private ?AuditLogger $auditLogger = null;
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('Deploy with automatic health check and rollback on failure');
|
||||
$this->addArgument('--path', 'Repository root', '.');
|
||||
$this->addArgument('--env', 'Target environment: dev, demo, rs, live', '');
|
||||
$this->addArgument('--config', 'Explicit sftp-config path (overrides --env)', '');
|
||||
$this->addArgument('--url', 'Site URL for health check', '');
|
||||
$this->addArgument('--checks', 'Health checks: http,admin,api (comma-sep)', 'http');
|
||||
$this->addArgument('--timeout', 'Health check timeout in seconds', '30');
|
||||
$this->addArgument('--retries', 'Health check retries before rollback', '2');
|
||||
$this->addArgument('--delay', 'Seconds between health check retries', '5');
|
||||
}
|
||||
|
||||
protected function run(): int
|
||||
{
|
||||
$path = realpath($this->getArgument('--path', '.')) ?: '.';
|
||||
$env = $this->getArgument('--env', '');
|
||||
$config = $this->getArgument('--config', '');
|
||||
$url = $this->getArgument('--url', '');
|
||||
$checks = $this->getArgument('--checks', 'http');
|
||||
$timeout = (int) $this->getArgument('--timeout', '30');
|
||||
$retries = (int) $this->getArgument('--retries', '2');
|
||||
$delay = (int) $this->getArgument('--delay', '5');
|
||||
|
||||
if ($url === '') {
|
||||
$this->log('ERROR', 'The --url argument is required for health checks');
|
||||
return self::EXIT_USAGE;
|
||||
}
|
||||
|
||||
if ($env === '' && $config === '') {
|
||||
$this->log('ERROR', 'Specify --env or --config for the deploy target');
|
||||
return self::EXIT_USAGE;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->auditLogger = new AuditLogger('deploy-and-verify');
|
||||
} catch (\Exception $e) {
|
||||
// Non-fatal — proceed without audit logging
|
||||
}
|
||||
|
||||
$this->audit('start', ['path' => $path, 'env' => $env, 'url' => parse_url($url, PHP_URL_HOST) ?? $url]);
|
||||
|
||||
// ── Build subprocess args ────────────────────────────────────
|
||||
$deployArgs = $this->buildDeployArgs($path, $env, $config);
|
||||
|
||||
// ── Step 1: Backup ───────────────────────────────────────────
|
||||
$this->section('Step 1: Pre-deploy backup');
|
||||
$snapshotDir = sys_get_temp_dir() . '/moko_deploy_snapshot_' . date('Ymd_His') . '_' . getmypid() . '_' . bin2hex(random_bytes(4));
|
||||
|
||||
if ($this->dryRun) {
|
||||
$this->log('INFO', "[dry-run] Would create snapshot at {$snapshotDir}");
|
||||
} else {
|
||||
$backupExit = $this->runSubprocess('backup-before-deploy.php', array_merge(
|
||||
$deployArgs, ['--snapshot-dir', $snapshotDir]
|
||||
));
|
||||
|
||||
if ($backupExit !== 0) {
|
||||
$this->log('ERROR', 'Pre-deploy backup failed — aborting deployment');
|
||||
$this->audit('backup_failed', ['exit_code' => $backupExit]);
|
||||
return self::EXIT_FAILURE;
|
||||
}
|
||||
$this->log('INFO', "Snapshot saved to {$snapshotDir}");
|
||||
}
|
||||
|
||||
// ── Step 2: Deploy ───────────────────────────────────────────
|
||||
$this->section('Step 2: Deploy');
|
||||
|
||||
if ($this->dryRun) {
|
||||
$this->log('INFO', '[dry-run] Would run deploy-sftp.php ' . implode(' ', $deployArgs));
|
||||
} else {
|
||||
$deployExit = $this->runSubprocess('deploy-sftp.php', $deployArgs);
|
||||
|
||||
if ($deployExit !== 0) {
|
||||
$this->log('ERROR', 'Deploy failed — rolling back to pre-deploy state');
|
||||
$this->audit('deploy_failed', ['exit_code' => $deployExit]);
|
||||
$this->runSubprocess('rollback-joomla.php', array_merge(
|
||||
$deployArgs, ['--snapshot-dir', $snapshotDir]
|
||||
));
|
||||
$this->cleanup($snapshotDir);
|
||||
return self::EXIT_FAILURE;
|
||||
}
|
||||
$this->log('INFO', 'Deploy completed successfully');
|
||||
}
|
||||
|
||||
// ── Step 3: Health check (with retries) ──────────────────────
|
||||
$this->section('Step 3: Health check');
|
||||
|
||||
if ($this->dryRun) {
|
||||
$this->log('INFO', "[dry-run] Would check {$url} with checks: {$checks}");
|
||||
$this->log('INFO', '[dry-run] Deploy-and-verify complete');
|
||||
return self::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
$healthy = false;
|
||||
for ($attempt = 1; $attempt <= $retries; $attempt++) {
|
||||
$this->log('INFO', "Health check attempt {$attempt}/{$retries}...");
|
||||
|
||||
if ($attempt > 1) {
|
||||
$this->log('INFO', "Waiting {$delay}s before retry...");
|
||||
sleep($delay);
|
||||
}
|
||||
|
||||
$healthExit = $this->runHealthCheck($url, $checks, $timeout);
|
||||
|
||||
if ($healthExit === 0) {
|
||||
$healthy = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$this->log('WARNING', "Health check attempt {$attempt} failed (exit {$healthExit})");
|
||||
}
|
||||
|
||||
if ($healthy) {
|
||||
$this->section('Result: SUCCESS');
|
||||
$this->log('INFO', 'Health check passed — deploy verified');
|
||||
$this->audit('success', ['url' => $url, 'attempts' => $attempt]);
|
||||
$this->cleanup($snapshotDir);
|
||||
return self::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
// ── Step 4: Rollback ─────────────────────────────────────────
|
||||
$this->section('Step 4: ROLLBACK');
|
||||
$this->log('ERROR', "Health check failed after {$retries} attempts — rolling back");
|
||||
$this->audit('rollback_triggered', ['url' => $url, 'retries' => $retries]);
|
||||
|
||||
$rollbackExit = $this->runSubprocess('rollback-joomla.php', array_merge(
|
||||
$deployArgs, ['--snapshot-dir', $snapshotDir]
|
||||
));
|
||||
|
||||
if ($rollbackExit === 0) {
|
||||
$this->log('INFO', 'Rollback completed — site restored to pre-deploy state');
|
||||
$this->audit('rollback_success', []);
|
||||
|
||||
// Verify rollback worked
|
||||
$postRollbackHealth = $this->runHealthCheck($url, $checks, $timeout);
|
||||
if ($postRollbackHealth === 0) {
|
||||
$this->log('INFO', 'Post-rollback health check passed — site is healthy');
|
||||
} else {
|
||||
$this->log('ERROR', 'Post-rollback health check FAILED — manual intervention needed');
|
||||
$this->audit('rollback_verification_failed', []);
|
||||
}
|
||||
} else {
|
||||
$this->log('ERROR', 'Rollback FAILED — manual intervention required');
|
||||
$this->audit('rollback_failed', ['exit_code' => $rollbackExit]);
|
||||
}
|
||||
|
||||
$this->cleanup($snapshotDir);
|
||||
return self::EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// ── Health check (inline, no subprocess) ─────────────────────────
|
||||
|
||||
private function runHealthCheck(string $url, string $checks, int $timeout): int
|
||||
{
|
||||
$url = rtrim($url, '/');
|
||||
$checkList = array_map('trim', explode(',', $checks));
|
||||
$failed = 0;
|
||||
|
||||
foreach ($checkList as $check) {
|
||||
$checkUrl = match ($check) {
|
||||
'admin' => $url . '/administrator/',
|
||||
'api' => $url . '/api/index.php/v1',
|
||||
default => $url,
|
||||
};
|
||||
|
||||
$result = $this->httpGet($checkUrl, $timeout);
|
||||
|
||||
if ($result === null) {
|
||||
$this->log('ERROR', " [{$check}] FAIL: connection failed");
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$validCodes = ($check === 'api') ? [200, 401] : [200];
|
||||
if (!in_array($result['http_code'], $validCodes, true)) {
|
||||
$this->log('ERROR', " [{$check}] FAIL: HTTP {$result['http_code']}");
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->containsFatalError($result['body'])) {
|
||||
$this->log('ERROR', " [{$check}] FAIL: PHP fatal error in response");
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->log('INFO', " [{$check}] PASS: HTTP {$result['http_code']} ({$result['time_ms']}ms)");
|
||||
}
|
||||
|
||||
return $failed > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
private function httpGet(string $url, int $timeout): ?array
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLOPT_TIMEOUT => $timeout,
|
||||
CURLOPT_CONNECTTIMEOUT => $timeout,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_USERAGENT => 'MokoDeployVerify/1.0',
|
||||
]);
|
||||
|
||||
$body = curl_exec($ch);
|
||||
if (curl_errno($ch)) {
|
||||
curl_close($ch);
|
||||
return null;
|
||||
}
|
||||
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$totalTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME);
|
||||
curl_close($ch);
|
||||
|
||||
return [
|
||||
'http_code' => $httpCode,
|
||||
'body' => is_string($body) ? $body : '',
|
||||
'time_ms' => (int) round($totalTime * 1000),
|
||||
];
|
||||
}
|
||||
|
||||
private function containsFatalError(string $body): bool
|
||||
{
|
||||
foreach (['Fatal error:', 'Fatal Error', 'Parse error:', 'Uncaught Error:', 'Uncaught Exception:'] as $pattern) {
|
||||
if (stripos($body, $pattern) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Subprocess helpers ───────────────────────────────────────────
|
||||
|
||||
private function runSubprocess(string $script, array $args): int
|
||||
{
|
||||
$scriptPath = __DIR__ . '/' . $script;
|
||||
if (!is_file($scriptPath)) {
|
||||
$this->log('ERROR', "Script not found: {$scriptPath}");
|
||||
return 127;
|
||||
}
|
||||
|
||||
$cmd = sprintf('php %s %s 2>&1',
|
||||
escapeshellarg($scriptPath),
|
||||
implode(' ', array_map('escapeshellarg', $args))
|
||||
);
|
||||
|
||||
$this->log('DEBUG', "Running: {$cmd}");
|
||||
passthru($cmd, $exitCode);
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
private function buildDeployArgs(string $path, string $env, string $config): array
|
||||
{
|
||||
$args = ['--path', $path];
|
||||
if ($config !== '') {
|
||||
$args[] = '--config';
|
||||
$args[] = $config;
|
||||
} elseif ($env !== '') {
|
||||
$args[] = '--env';
|
||||
$args[] = $env;
|
||||
}
|
||||
if ($this->dryRun) {
|
||||
$args[] = '--dry-run';
|
||||
}
|
||||
return $args;
|
||||
}
|
||||
|
||||
// ── Audit ────────────────────────────────────────────────────────
|
||||
|
||||
private function audit(string $event, array $data): void
|
||||
{
|
||||
if ($this->auditLogger === null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->auditLogger->logInfo("deploy-verify:{$event}", $data);
|
||||
} catch (\Exception $e) {
|
||||
// Non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cleanup ──────────────────────────────────────────────────────
|
||||
|
||||
private function cleanup(string $snapshotDir): void
|
||||
{
|
||||
if (is_dir($snapshotDir)) {
|
||||
$this->removeDirectory($snapshotDir);
|
||||
$this->log('DEBUG', "Cleaned up snapshot: {$snapshotDir}");
|
||||
}
|
||||
}
|
||||
|
||||
private function removeDirectory(string $dir): void
|
||||
{
|
||||
$entries = scandir($dir);
|
||||
if ($entries === false) {
|
||||
return;
|
||||
}
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry === '.' || $entry === '..') {
|
||||
continue;
|
||||
}
|
||||
$path = $dir . DIRECTORY_SEPARATOR . $entry;
|
||||
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
|
||||
}
|
||||
rmdir($dir);
|
||||
}
|
||||
}
|
||||
|
||||
$app = new DeployAndVerify();
|
||||
exit($app->execute());
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /deploy/deploy-dolibarr.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Deploy Dolibarr module files to a remote server via SFTP/rsync
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /deploy/health-check.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Post-deploy health check — verify a Joomla site is responding correctly
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /deploy/rollback-joomla.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Rollback a Joomla deployment by restoring from a pre-deploy snapshot
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /deploy/sync-joomla.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Sync Joomla site directories between two servers via rsync over SSH
|
||||
*/
|
||||
|
||||
|
||||
@@ -590,25 +590,6 @@ abstract class CliFramework
|
||||
$this->display(' ' . $this->c($color . self::C_BOLD, $icon) . ' ' . $label . $suffix . "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a recovery suggestion with a lightbulb prefix.
|
||||
*
|
||||
* @param string $suggestion Fix suggestion text (from RecoverySuggestion)
|
||||
*/
|
||||
protected function suggest(string $suggestion): void
|
||||
{
|
||||
if ($this->quiet) {
|
||||
return;
|
||||
}
|
||||
$this->clearProgress();
|
||||
$lines = explode("\n", $suggestion);
|
||||
$first = array_shift($lines);
|
||||
$this->display(' ' . $this->c(self::C_YELLOW, '💡 ' . $first) . "\n");
|
||||
foreach ($lines as $line) {
|
||||
$this->display(' ' . $this->c(self::C_YELLOW, ' ' . $line) . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Console graphics — progress bar
|
||||
// =========================================================================
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
#!/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.Enterprise
|
||||
* INGROUP: MokoPlatform.Lib
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
* PATH: /lib/Enterprise/RecoverySuggestion.php
|
||||
* BRIEF: Smart error recovery suggestions for validators
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MokoCli;
|
||||
|
||||
/**
|
||||
* Generates actionable fix suggestions when validators detect problems.
|
||||
*
|
||||
* Each method returns a human-readable suggestion string that tells the
|
||||
* developer exactly what to do to fix the issue.
|
||||
*/
|
||||
class RecoverySuggestion
|
||||
{
|
||||
/**
|
||||
* Suggest creating a missing required file.
|
||||
*/
|
||||
public static function forMissingFile(string $file, string $template = ''): string
|
||||
{
|
||||
$suggestion = "Create the missing file: {$file}";
|
||||
if ($template !== '') {
|
||||
$suggestion .= "\n Copy from template: {$template}";
|
||||
}
|
||||
return $suggestion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest adding a missing XML element.
|
||||
*/
|
||||
public static function forMissingXmlElement(string $element, string $value, string $file, int $afterLine = 0): string
|
||||
{
|
||||
$snippet = "<{$element}>{$value}</{$element}>";
|
||||
if ($afterLine > 0) {
|
||||
return "Add {$snippet} after line {$afterLine} in {$file}";
|
||||
}
|
||||
return "Add {$snippet} to {$file}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest fixing a version mismatch.
|
||||
*/
|
||||
public static function forVersionMismatch(string $file, string $found, string $expected): string
|
||||
{
|
||||
return "Update version in {$file}: change \"{$found}\" to \"{$expected}\"";
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest creating a missing directory.
|
||||
*/
|
||||
public static function forMissingDirectory(string $dir): string
|
||||
{
|
||||
return "Create the missing directory:\n mkdir -p {$dir}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest fixing a syntax error.
|
||||
*/
|
||||
public static function forSyntaxError(string $file, int $line, string $error): string
|
||||
{
|
||||
return "Fix syntax error at {$file}:{$line}\n {$error}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest fixing a missing license header.
|
||||
*/
|
||||
public static function forMissingHeader(string $file): string
|
||||
{
|
||||
return "Add SPDX license header to {$file}:\n /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>\n * SPDX-License-Identifier: GPL-3.0-or-later */";
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest running a command to fix an issue.
|
||||
*/
|
||||
public static function forCommand(string $command, string $context = ''): string
|
||||
{
|
||||
$suggestion = "Run: {$command}";
|
||||
if ($context !== '') {
|
||||
$suggestion = "{$context}\n {$suggestion}";
|
||||
}
|
||||
return $suggestion;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
DEFGROUP: dolibarr-api-mcp.Documentation
|
||||
INGROUP: dolibarr-api-mcp
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
||||
VERSION: 09.37.00
|
||||
VERSION: 09.34.01
|
||||
PATH: ./CONTRIBUTING.md
|
||||
BRIEF: Contribution guidelines for the project
|
||||
-->
|
||||
|
||||
@@ -10,7 +10,7 @@ DEFGROUP: dolibarr-api-mcp.Documentation
|
||||
INGROUP: dolibarr-api-mcp
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 09.37.00
|
||||
VERSION: 09.34.01
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
DEFGROUP:
|
||||
INGROUP: Project.Documentation
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCli-Template-Generic
|
||||
VERSION: 09.37.00
|
||||
VERSION: 09.34.01
|
||||
PATH: ./CONTRIBUTING.md
|
||||
BRIEF: Contribution guidelines for the project
|
||||
-->
|
||||
|
||||
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
|
||||
INGROUP: [PROJECT_NAME].Documentation
|
||||
REPO: [REPOSITORY_URL]
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 09.37.00
|
||||
VERSION: 09.34.01
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class VersionBumpTest extends TestCase
|
||||
{
|
||||
file_put_contents(
|
||||
"{$this->tmpDir}/README.md",
|
||||
"<!-- VERSION: 09.37.00 -->\nSome content\n"
|
||||
"<!-- VERSION: 09.34.01 -->\nSome content\n"
|
||||
);
|
||||
|
||||
$this->execute();
|
||||
|
||||
@@ -34,7 +34,7 @@ class VersionReadTest extends TestCase
|
||||
{
|
||||
file_put_contents(
|
||||
"{$this->tmpDir}/README.md",
|
||||
"# Test\n<!-- VERSION: 09.37.00 -->\n"
|
||||
"# Test\n<!-- VERSION: 09.34.01 -->\n"
|
||||
);
|
||||
|
||||
$this->assertSame('02.03.04', trim($this->runScript()));
|
||||
@@ -68,7 +68,7 @@ class VersionReadTest extends TestCase
|
||||
{
|
||||
file_put_contents(
|
||||
"{$this->tmpDir}/README.md",
|
||||
"<!-- VERSION: 09.37.00 -->\n"
|
||||
"<!-- VERSION: 09.34.01 -->\n"
|
||||
);
|
||||
mkdir("{$this->tmpDir}/src", 0755, true);
|
||||
file_put_contents(
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* INGROUP: MokoPlatform
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||
* PATH: /validate/check_file_integrity.php
|
||||
* VERSION: 09.37.00
|
||||
* VERSION: 09.34.01
|
||||
* BRIEF: Compare deployed files on a remote server against the local repository to detect drift
|
||||
*/
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use MokoCli\CliFramework;
|
||||
use MokoCli\RecoverySuggestion;
|
||||
|
||||
/**
|
||||
* Validates that the required directories and files exist in the repository root.
|
||||
@@ -68,7 +67,6 @@ class CheckStructure extends CliFramework
|
||||
if (!is_dir($path . '/' . $dir)) {
|
||||
$missingDirs[] = $dir;
|
||||
$this->status(false, "Directory: {$dir}");
|
||||
$this->suggest(RecoverySuggestion::forMissingDirectory($dir));
|
||||
$failed++;
|
||||
} else {
|
||||
$this->status(true, "Directory: {$dir}");
|
||||
@@ -98,7 +96,6 @@ class CheckStructure extends CliFramework
|
||||
if (!is_file($path . '/' . $file)) {
|
||||
$missingFiles[] = $file;
|
||||
$this->status(false, "File: {$file}");
|
||||
$this->suggest(RecoverySuggestion::forMissingFile($file));
|
||||
$failed++;
|
||||
} else {
|
||||
$this->status(true, "File: {$file}");
|
||||
|
||||
@@ -21,7 +21,6 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use MokoCli\CliFramework;
|
||||
use MokoCli\RecoverySuggestion;
|
||||
|
||||
/**
|
||||
* Checks that the version recorded in composer.json matches VERSION headers
|
||||
@@ -102,7 +101,6 @@ class CheckVersionConsistency extends CliFramework
|
||||
if ($match[0] !== $expected) {
|
||||
$line = substr_count(substr($content, 0, (int) $match[1]), "\n") + 1;
|
||||
$this->status(false, $filename, "line {$line}: found {$match[0]}, expected {$expected}");
|
||||
$this->suggest(RecoverySuggestion::forVersionMismatch($filename, $match[0], $expected));
|
||||
$issues[] = $filename;
|
||||
$filePassed = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user