Files
AirCoding/packages/cli/src/commands/doctor.ts
AirCoding 0887522b30 feat(doctor): permissioned fix mode — §6.12 compliance
- --fix 先显示所有 fix 方案,再 readline 询问用户确认
- 拒绝直接执行,需用户输入 y 才继续
- DoctorService.fix() 支持 toolchain.* 工具通过 apt 安装
- 支持 display (ImageMagick) 安装
- fix 后自动重跑 diagnostics 显示更新状态
- 输出按 category 分组显示

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 18:32:22 +08:00

94 lines
3.1 KiB
TypeScript
Executable File

/**
* DoctorCommand - Diagnostic and repair command
* DD §17. doctor [--fix|--bundle].
*
* @module packages/cli/src/commands/doctor
*/
import { loadConfig } from '../bootstrap/loadConfig.js'
import { DoctorService } from '@aircoding/runtime'
import { createInterface } from 'readline'
async function ask_user(prompt: string): Promise<boolean> {
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<void> {
const config = loadConfig()
const project_root = config.project_root || process.cwd()
const doctor = new DoctorService(project_root)
console.log('Running diagnostics...\n')
const report = await doctor.run_diagnostics(options.scope as any || 'all')
// Group checks by category for clean output
const categories = new Map<string, Array<typeof report.checks[0]>>()
for (const check of report.checks) {
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'}`)
console.log(`All checks: ${report.all_passed ? '✅ PASS' : '❌ FAIL'}`)
console.log(`Fixable: ${report.fixable_count} issues`)
if (options.fix) {
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) {
console.log('\nBundle feature not yet implemented (P8)')
}
}