From 0887522b30daa048f64b6cba4ed6d6d2a9d93a22 Mon Sep 17 00:00:00 2001 From: AirCoding Date: Tue, 9 Jun 2026 18:32:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(doctor):=20permissioned=20fix=20mode=20?= =?UTF-8?q?=E2=80=94=20=C2=A76.12=20compliance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - --fix 先显示所有 fix 方案,再 readline 询问用户确认 - 拒绝直接执行,需用户输入 y 才继续 - DoctorService.fix() 支持 toolchain.* 工具通过 apt 安装 - 支持 display (ImageMagick) 安装 - fix 后自动重跑 diagnostics 显示更新状态 - 输出按 category 分组显示 Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/cli/src/commands/doctor.ts | 68 +++++++++++++++++--- packages/runtime/src/doctor/DoctorService.ts | 20 ++++++ 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index 5983c6e..b0831b9 100755 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -7,6 +7,17 @@ import { loadConfig } from '../bootstrap/loadConfig.js' import { DoctorService } from '@aircoding/runtime' +import { createInterface } from 'readline' + +async function ask_user(prompt: string): Promise { + const rl = createInterface({ input: process.stdin, output: process.stdout }) + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + rl.close() + resolve(answer.toLowerCase().startsWith('y')) + }) + }) +} export async function doctorCommand(options: { fix?: boolean; bundle?: boolean; scope?: string }): Promise { const config = loadConfig() @@ -17,10 +28,21 @@ export async function doctorCommand(options: { fix?: boolean; bundle?: boolean; const report = await doctor.run_diagnostics(options.scope as any || 'all') + // Group checks by category for clean output + const categories = new Map>() for (const check of report.checks) { - const icon = check.passed ? '✅' : '❌' - const fixable = check.fixable ? ' [fixable]' : '' - console.log(` ${icon} ${check.name}: ${check.message}${fixable}`) + const cat = categories.get(check.category) || [] + cat.push(check) + categories.set(check.category, cat) + } + + for (const [category, checks] of categories) { + console.log(` [${category}]`) + for (const check of checks) { + const icon = check.passed ? '✅' : '❌' + const fixHint = check.fixable ? ` → fix: ${check.fix || 'manual'}` : '' + console.log(` ${icon} ${check.name}: ${check.message}${fixHint}`) + } } console.log(`\nBootstrap: ${report.bootstrap_passed ? '✅ PASS' : '❌ FAIL'}`) @@ -28,13 +50,41 @@ export async function doctorCommand(options: { fix?: boolean; bundle?: boolean; console.log(`Fixable: ${report.fixable_count} issues`) if (options.fix) { - console.log('\nAttempting fixes...') - for (const check of report.checks) { - if (!check.passed && check.fixable) { - const result = await doctor.fix(check.name) - console.log(` ${result.ok ? '✅' : '❌'} ${check.name}: ${result.message}`) - } + const fixable = report.checks.filter(c => !c.passed && c.fixable) + if (fixable.length === 0) { + console.log('\nNothing to fix.') + return } + + console.log(`\n${fixable.length} fixable issue(s) found:`) + for (const check of fixable) { + console.log(` - ${check.name}: ${check.fix || 'manual fix required'}`) + } + + // Permissioned fix mode: ask user before each fix (§6.12) + const approved = await ask_user(`\nApply these fixes? This may install system packages. [y/N] `) + if (!approved) { + console.log('Fix cancelled.') + return + } + + console.log('\nApplying fixes...') + for (const check of fixable) { + const result = await doctor.fix(check.name) + const icon = result.ok ? '✅' : '❌' + console.log(` ${icon} ${check.name}: ${result.message}`) + } + + // Re-run diagnostics to show updated state + console.log('\nRe-running diagnostics...\n') + const updated = await doctor.run_diagnostics(options.scope as any || 'all') + for (const check of updated.checks.filter(c => !c.passed)) { + console.log(` ❌ ${check.name}: ${check.message}`) + } + if (updated.all_passed) { + console.log(' ✅ All checks passed after fix!') + } + console.log(`\nUpdated: ${updated.all_passed ? '✅ PASS' : '❌ FAIL'}`) } if (options.bundle) { diff --git a/packages/runtime/src/doctor/DoctorService.ts b/packages/runtime/src/doctor/DoctorService.ts index 5acf2d9..521e915 100755 --- a/packages/runtime/src/doctor/DoctorService.ts +++ b/packages/runtime/src/doctor/DoctorService.ts @@ -112,7 +112,27 @@ export class DoctorService { case 'project_structure': { return { ok: false, message: 'Run air init to create project structure' } } + case 'display': { + try { + execFileSync('sudo', ['apt', 'install', '-y', 'imagemagick'], { stdio: 'pipe', timeout: 60000 }) + return { ok: true, message: 'ImageMagick installed' } + } catch (e: any) { + return { ok: false, message: `ImageMagick install failed: ${e.message}` } + } + } default: + // Toolchain fix: try apt install + if (check_name.startsWith('toolchain.')) { + const pkg = check_name.replace('toolchain.', '') + const pkgMap: Record = { cmake: 'cmake', ninja: 'ninja-build', cppcheck: 'cppcheck', clangd: 'clangd', 'g++': 'g++' } + const aptPkg = pkgMap[pkg] || pkg + try { + execFileSync('sudo', ['apt', 'install', '-y', aptPkg], { stdio: 'pipe', timeout: 120000 }) + return { ok: true, message: `${pkg} installed via apt` } + } catch (e: any) { + return { ok: false, message: `Failed to install ${pkg}: ${e.message}` } + } + } return { ok: false, message: `Fix for ${check_name} not implemented` } } }