Files
ulthon_admin/extend/base/common/command/tools/agent/ToolsAgentPublishBase.php
2026-03-26 20:22:34 +08:00

278 lines
9.1 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace base\common\command\tools\agent;
use app\common\console\Command;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\facade\App;
class ToolsAgentPublishBase extends Command
{
protected function configure()
{
parent::configure();
$this->setName('tools:agent:publish')
->setDescription('发布 AGENTS.md 与 .agent 到兼容目标(复制 rules/skills可选生成 CLAUDE.md')
->addOption('target', 't', Option::VALUE_OPTIONAL, '发布目标trae|opencode|roocode|cursor|claude|all', 'all')
->addOption('dry-run', null, Option::VALUE_NONE, '只输出将要写入/覆盖的清单,不落盘')
->addOption('clean', null, Option::VALUE_NONE, '清理目标中由发布器生成的内容(仅影响 rules/skills 与发布文件)')
->addOption('force', 'f', Option::VALUE_NONE, '跳过覆盖确认');
}
protected function execute(Input $input, Output $output)
{
$rootPath = rtrim(App::getRootPath(), "\\/ \t\n\r\0\x0B");
$sourceAgentsPath = $rootPath . DIRECTORY_SEPARATOR . 'AGENTS.md';
$sourceRulesDir = $rootPath . DIRECTORY_SEPARATOR . '.agent' . DIRECTORY_SEPARATOR . 'rules';
$sourceSkillsDir = $rootPath . DIRECTORY_SEPARATOR . '.agent' . DIRECTORY_SEPARATOR . 'skills';
if (!is_file($sourceAgentsPath)) {
$output->error('缺少单一事实来源文件:' . $sourceAgentsPath);
return;
}
if (!is_dir($sourceRulesDir)) {
$output->error('缺少单一事实来源目录:' . $sourceRulesDir);
return;
}
if (!is_dir($sourceSkillsDir)) {
$output->error('缺少单一事实来源目录:' . $sourceSkillsDir);
return;
}
$target = strtolower((string)$input->getOption('target'));
if ($target === '') {
$target = 'all';
}
$supportedTargets = ['trae', 'opencode', 'roocode', 'cursor', 'claude', 'all'];
if (!in_array($target, $supportedTargets, true)) {
$output->error('不支持的 target' . $target . '(可选:' . implode('|', $supportedTargets) . '');
return;
}
$dryRun = (bool)$input->getOption('dry-run');
$clean = (bool)$input->getOption('clean');
$targets = $target === 'all' ? ['trae', 'opencode', 'roocode', 'cursor', 'claude'] : [$target];
$writeActions = [];
$deleteActions = [];
$agentsContent = file_get_contents($sourceAgentsPath);
if ($agentsContent === false) {
$output->error('读取失败:' . $sourceAgentsPath);
return;
}
foreach ($targets as $t) {
if (in_array($t, ['trae', 'opencode', 'roocode', 'cursor'], true)) {
$distRoot = $rootPath . DIRECTORY_SEPARATOR . '.' . $t;
$distRules = $distRoot . DIRECTORY_SEPARATOR . 'rules';
$distSkills = $distRoot . DIRECTORY_SEPARATOR . 'skills';
if ($t === 'cursor') {
$deleteActions[] = $distRules . DIRECTORY_SEPARATOR . 'ulthon-framework.mdc';
}
if ($clean) {
$deleteActions[] = $distRules;
$deleteActions[] = $distSkills;
}
$writeActions = array_merge($writeActions, $this->planCopyDir($sourceRulesDir, $distRules));
$writeActions = array_merge($writeActions, $this->planCopyDir($sourceSkillsDir, $distSkills));
}
if ($t === 'claude') {
$distClaude = $rootPath . DIRECTORY_SEPARATOR . 'CLAUDE.md';
if ($clean) {
$deleteActions[] = $distClaude;
}
$writeActions = array_merge($writeActions, $this->planWriteFile($distClaude, $agentsContent));
}
}
$deleteActions = array_values(array_unique(array_filter($deleteActions, 'strlen')));
$plannedDeletes = $this->filterExistingDeletes($deleteActions);
$plannedWrites = $this->filterEffectiveWrites($writeActions, $clean);
$output->writeln('');
$output->writeln('<info>=== tools:agent:publish 计划 ===</info>');
$output->writeln('target' . $target);
$output->writeln('dry-run' . ($dryRun ? 'yes' : 'no'));
$output->writeln('clean' . ($clean ? 'yes' : 'no'));
$output->writeln('');
if (empty($plannedDeletes) && empty($plannedWrites)) {
$output->writeln('<comment>无变更:目标已是最新状态。</comment>');
$output->writeln('');
return;
}
if (!empty($plannedDeletes)) {
$output->writeln('<comment>将删除:</comment>');
foreach ($plannedDeletes as $path) {
$output->writeln(' - ' . $path);
}
$output->writeln('');
}
if (!empty($plannedWrites)) {
$output->writeln('<comment>将写入:</comment>');
foreach ($plannedWrites as $item) {
$output->writeln(' - [' . $item['mode'] . '] ' . $item['dist']);
}
$output->writeln('');
}
if ($dryRun) {
return;
}
if (!$output->confirm($input, '将执行上述写入/覆盖/删除操作,是否继续?', true)) {
$output->comment('已取消。');
$output->newLine();
return false;
}
foreach ($plannedDeletes as $path) {
$this->deletePath($path);
}
foreach ($plannedWrites as $item) {
$this->ensureDir(dirname($item['dist']));
file_put_contents($item['dist'], $item['content']);
}
$output->info('发布完成。');
$output->newLine();
}
protected function planCopyDir(string $sourceDir, string $distDir): array
{
$actions = [];
if (!is_dir($sourceDir)) {
return $actions;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
$sourceDirNormalized = rtrim($sourceDir, "\\/") . DIRECTORY_SEPARATOR;
foreach ($iterator as $fileInfo) {
if (!$fileInfo->isFile()) {
continue;
}
$src = $fileInfo->getPathname();
$relative = substr($src, strlen($sourceDirNormalized));
$dist = $distDir . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relative);
$content = file_get_contents($src);
if ($content === false) {
continue;
}
$actions[] = [
'dist' => $dist,
'content' => $content,
];
}
return $actions;
}
protected function planWriteFile(string $dist, string $content): array
{
return [[
'dist' => $dist,
'content' => $content,
]];
}
protected function filterEffectiveWrites(array $actions, bool $forceWrite = false): array
{
$result = [];
$resultMap = [];
foreach ($actions as $item) {
$dist = (string)($item['dist'] ?? '');
$content = (string)($item['content'] ?? '');
if ($dist === '') {
continue;
}
$mode = 'create';
if (is_file($dist)) {
$existing = file_get_contents($dist);
if (!$forceWrite && $existing !== false && $existing === $content) {
continue;
}
$mode = 'update';
}
$resultMap[$dist] = [
'dist' => $dist,
'content' => $content,
'mode' => $mode,
];
}
$result = array_values($resultMap);
return $result;
}
protected function filterExistingDeletes(array $deleteActions): array
{
$result = [];
foreach ($deleteActions as $path) {
if (is_file($path) || is_dir($path)) {
$result[] = $path;
}
}
return $result;
}
protected function deletePath(string $path): void
{
if (is_file($path)) {
@unlink($path);
return;
}
if (!is_dir($path)) {
return;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $fileInfo) {
if ($fileInfo->isDir()) {
@rmdir($fileInfo->getPathname());
continue;
}
@unlink($fileInfo->getPathname());
}
@rmdir($path);
}
protected function ensureDir(string $dir): void
{
if ($dir === '' || is_dir($dir)) {
return;
}
@mkdir($dir, 0777, true);
}
}