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>
This commit is contained in:
@@ -7,6 +7,17 @@
|
|||||||
|
|
||||||
import { loadConfig } from '../bootstrap/loadConfig.js'
|
import { loadConfig } from '../bootstrap/loadConfig.js'
|
||||||
import { DoctorService } from '@aircoding/runtime'
|
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> {
|
export async function doctorCommand(options: { fix?: boolean; bundle?: boolean; scope?: string }): Promise<void> {
|
||||||
const config = loadConfig()
|
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')
|
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) {
|
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 icon = check.passed ? '✅' : '❌'
|
||||||
const fixable = check.fixable ? ' [fixable]' : ''
|
const fixHint = check.fixable ? ` → fix: ${check.fix || 'manual'}` : ''
|
||||||
console.log(` ${icon} ${check.name}: ${check.message}${fixable}`)
|
console.log(` ${icon} ${check.name}: ${check.message}${fixHint}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\nBootstrap: ${report.bootstrap_passed ? '✅ PASS' : '❌ FAIL'}`)
|
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`)
|
console.log(`Fixable: ${report.fixable_count} issues`)
|
||||||
|
|
||||||
if (options.fix) {
|
if (options.fix) {
|
||||||
console.log('\nAttempting fixes...')
|
const fixable = report.checks.filter(c => !c.passed && c.fixable)
|
||||||
for (const check of report.checks) {
|
if (fixable.length === 0) {
|
||||||
if (!check.passed && check.fixable) {
|
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 result = await doctor.fix(check.name)
|
||||||
console.log(` ${result.ok ? '✅' : '❌'} ${check.name}: ${result.message}`)
|
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) {
|
if (options.bundle) {
|
||||||
|
|||||||
@@ -112,7 +112,27 @@ export class DoctorService {
|
|||||||
case 'project_structure': {
|
case 'project_structure': {
|
||||||
return { ok: false, message: 'Run air init to create 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:
|
default:
|
||||||
|
// Toolchain fix: try apt install
|
||||||
|
if (check_name.startsWith('toolchain.')) {
|
||||||
|
const pkg = check_name.replace('toolchain.', '')
|
||||||
|
const pkgMap: Record<string, string> = { 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` }
|
return { ok: false, message: `Fix for ${check_name} not implemented` }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user