autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
1,223 lines • 73.3 kB
JavaScript
#!/usr/bin/env node
/**
* AutoSnippet V2 CLI
*
* Usage:
* asd setup - 初始化项目(--repo 指定子仓库远程地址)
* asd remote <url> - 将 recipes 目录转为独立子仓库并关联远程仓库
* asd coldstart - 冷启动知识库(9 维度分析 + AI 填充)
* asd rescan - 增量知识更新(保留 Recipe,重新扫描)
* asd ais [Target] - AI 扫描 Target → 直接发布 Recipes
* asd search <query> - 搜索知识库
* asd guard <file> - Guard 检查
* asd guard:ci [path] - CI/CD Guard 合规检查
* asd server - 启动 API 服务
* asd ui - 启动 Dashboard UI
* asd upgrade - 升级 IDE 集成
* asd mirror - 镜像 .cursor/ → .qoder/ .trae/
* asd status - 环境状态
* asd health - 综合健康报告
*/
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
import { join, resolve } from 'node:path';
import { Command } from 'commander';
import { cli } from '../lib/cli/CliLogger.js';
import { DASHBOARD_DIR, PACKAGE_ROOT } from '../lib/shared/package-root.js';
import { shutdown } from '../lib/shared/shutdown.js';
const pkgPath = join(PACKAGE_ROOT, 'package.json');
const pkg = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, 'utf8')) : { version: '2.0.0' };
// ─── 进程级错误兜底 ────────────────────────────────────
process.on('uncaughtException', (error) => {
process.stderr.write(`[asd] Uncaught Exception: ${error.message}\n`);
if (error.stack) {
process.stderr.write(`${error.stack}\n`);
}
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
const msg = reason instanceof Error ? reason.message : String(reason);
const stack = reason instanceof Error ? reason.stack : undefined;
process.stderr.write(`[asd] Unhandled Rejection: ${msg}\n`);
if (stack) {
process.stderr.write(`${stack}\n`);
}
process.exit(1);
});
// 优雅关闭 — 统一 shutdown 协调器
shutdown.install();
const program = new Command();
program.name('asd').description('AutoSnippet V2 - AI 知识库管理工具').version(pkg.version);
// ─────────────────────────────────────────────────────
// setup 命令
// ─────────────────────────────────────────────────────
program
.command('setup')
.description('初始化项目工作空间:目录结构、数据库、IDE 集成、模板')
.option('-d, --dir <path>', '项目目录', '.')
.option('--force', '强制覆盖已有配置')
.option('--seed', '预置示例 Recipe(冷启动推荐)')
.option('--repo <url>', 'recipes 子仓库的远程 Git 仓库地址(提供则 clone,不提供则为普通目录)')
.action(async (opts) => {
const { SetupService } = await import('../lib/cli/SetupService.js');
const service = new SetupService({
projectRoot: resolve(opts.dir),
force: opts.force,
seed: opts.seed,
subRepoUrl: opts.repo,
});
await service.run();
service.printSummary();
});
// ─────────────────────────────────────────────────────
// remote 命令 — 将 recipes 目录转为独立子仓库并关联远程仓库
// ─────────────────────────────────────────────────────
program
.command('remote <url>')
.description('将 recipes 目录转为独立子仓库并关联远程 Git 仓库')
.option('-d, --dir <path>', '项目目录', '.')
.action(async (url, opts) => {
const projectRoot = resolve(opts.dir);
const { execSync: exec } = await import('node:child_process');
const { resolveSubRepoPath, isGitRepo } = await import('../lib/shared/ProjectMarkers.js');
const subRepoPath = resolveSubRepoPath(projectRoot);
// 1. 校验目录存在
if (!existsSync(subRepoPath)) {
cli.error('recipes/ 目录不存在,请先运行 asd setup');
process.exit(1);
}
// 2. URL 格式验证
if (!/^(https?:\/\/.+|git@.+:.+)$/.test(url)) {
cli.error('无效的 Git 仓库地址(支持 HTTPS 和 SSH 格式)');
process.exit(1);
}
const gitExec = (args) => {
return exec(`git ${args}`, { cwd: subRepoPath, stdio: 'pipe', encoding: 'utf8' }).trim();
};
// 3. 已经是 git 仓库 → 只更新 remote
if (isGitRepo(subRepoPath)) {
try {
gitExec(`remote get-url origin`);
// origin 已存在 → set-url
gitExec(`remote set-url origin ${url}`);
}
catch {
// origin 不存在 → add
gitExec(`remote add origin ${url}`);
}
// 更新 config.json
_updateConfigUrl(projectRoot, url);
cli.log('✓ 已更新 remote origin');
cli.log(` ${url}`);
return;
}
// 4. 普通目录 → 初始化为 git 仓库(保留已有文件)
cli.log('正在将 recipes/ 转为独立子仓库...');
gitExec('init');
gitExec(`remote add origin ${url}`);
gitExec('add .');
try {
gitExec('commit -m "Init AutoSnippet recipes"');
}
catch {
/* 空目录时 commit 可能失败,无影响 */
}
// 5. 更新 config.json
_updateConfigUrl(projectRoot, url);
cli.log('✓ recipes/ 已转为独立子仓库');
cli.log(` remote origin → ${url}`);
cli.log('');
cli.log('后续步骤:');
cli.log(' 1. git push -u origin main');
cli.log(' 2. 在主仓库中选择一种方式管理 recipes/:');
cli.log(` • git submodule add ${url} AutoSnippet/recipes`);
cli.log(' • 或将 AutoSnippet/recipes/ 加入 .gitignore');
});
/** 更新 .autosnippet/config.json 中的 core.subRepoUrl 字段 */
function _updateConfigUrl(projectRoot, url) {
const configPath = join(projectRoot, '.autosnippet', 'config.json');
if (!existsSync(configPath)) {
return;
}
try {
const raw = readFileSync(configPath, 'utf-8');
const config = JSON.parse(raw);
if (!config.core) {
config.core = {};
}
config.core.subRepoUrl = url;
writeFileSync(configPath, JSON.stringify(config, null, 2));
}
catch {
/* config 解析失败不阻塞主流程 */
}
}
// ─────────────────────────────────────────────────────
// coldstart 命令 (Knowledge Bootstrap)
// ─────────────────────────────────────────────────────
program
.command('coldstart')
.description('冷启动知识库:9 维度项目分析 + AI 异步填充(与 Dashboard 点击冷启动流程一致)')
.option('-d, --dir <path>', '项目目录', '.')
.option('-m, --max-files <n>', '最大扫描文件数', '500')
.option('--dims <ids...>', '仅运行指定维度(逗号分隔或多次指定)')
.option('--skip-guard', '跳过 Guard 审计')
.option('--no-skills', '禁用 Skill 加载')
.option('--wait', '等待 AI 异步填充完成(默认骨架完成即退出)')
.option('--json', '以 JSON 格式输出结果')
.action(async (opts) => {
const projectRoot = resolve(opts.dir);
if (opts.skipGuard) {
cli.log('ℹ️ Guard 审计已跳过');
}
try {
const { bootstrap, container } = await initContainer({ projectRoot });
const ora = (await import('ora')).default;
const spinner = ora('Phase 1-4: 收集文件、AST 分析、SPM 依赖、Guard 审计...').start();
// 直接调用 bootstrap-internal handler(统一编排管线)
const { bootstrapKnowledge } = await import('../lib/external/mcp/handlers/bootstrap-internal.js');
const logger = container.get('logger');
const raw = await bootstrapKnowledge({ container, logger }, {
maxFiles: parseInt(opts.maxFiles, 10),
skipGuard: opts.skipGuard || false,
contentMaxLines: 120,
loadSkills: opts.skills !== false,
skipAsyncFill: !opts.wait,
dimensions: opts.dims,
});
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
const result = parsed?.data || parsed;
spinner.stop();
if (opts.json) {
cli.json(result);
}
else {
// 输出骨架报告
const report = result.report || {};
const targets = result.targets || [];
const langStats = result.languageStats || {};
const guardSummary = result.guardSummary;
const astSummary = result.astSummary;
const framework = result.analysisFramework || {};
cli.log('\n📊 Coldstart Report');
cli.log(`${'─'.repeat(50)}`);
if (targets.length > 0) {
cli.log(`\n Targets: ${targets.map((t) => t.name || t).join(', ')}`);
}
if (Object.keys(langStats).length > 0) {
const langParts = Object.entries(langStats)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([ext, count]) => `${ext}(${count})`);
cli.log(` Languages: ${langParts.join(', ')}`);
}
// AST 分析
if (astSummary) {
if (astSummary.metrics) {
cli.log(` AST Metrics: ${JSON.stringify(astSummary.metrics)}`);
}
}
// SPM 依赖
if (report.phases?.spmDependencyGraph) {
const spm = report.phases.spmDependencyGraph;
cli.log(` SPM Dependencies: ${spm.packageCount ?? '?'} packages`);
}
// Guard 审计
if (guardSummary) {
cli.log(` Guard: ${guardSummary.totalViolations ?? guardSummary.total ?? '?'} violations (${guardSummary.errors ?? '?'} errors, ${guardSummary.warnings ?? '?'} warnings)`);
}
// 维度分析框架
if (framework.dimensions) {
cli.log('\n Analysis Dimensions:');
for (const dim of framework.dimensions) {
const type = dim.skillWorthy ? (dim.dualOutput ? 'Dual' : 'Skill') : 'Candidate';
cli.log(` ${type.padEnd(10)} ${dim.id || dim.name || '?'}`);
}
}
if (result.bootstrapSession) {
const session = result.bootstrapSession;
cli.log(`\n Session: ${session.id || 'N/A'} (${session.status || 'unknown'})`);
}
cli.blank();
}
// 等待模式: 轮询 BootstrapTaskManager 直到所有维度完成
if (opts.wait && result.bootstrapSession) {
const ora2 = (await import('ora')).default;
const waitSpinner = ora2('Phase 5: AI 正在逐维度填充知识...').start();
let lastStatus = '';
let attempts = 0;
const maxAttempts = Infinity; // 不限时——冷启动/增量扫描本身就耗时较长
while (attempts < maxAttempts) {
await new Promise((r) => setTimeout(r, 1000));
attempts++;
try {
const taskManager = container.get('bootstrapTaskManager');
const sessionStatus = taskManager.getSessionStatus();
if (!sessionStatus || !('tasks' in sessionStatus)) {
break;
}
const total = sessionStatus.tasks.length;
const done = sessionStatus.tasks.filter((t) => t.status === 'done' || t.status === 'error').length;
const current = sessionStatus.tasks.find((t) => t.status === 'running');
const statusText = current
? `[${done}/${total}] 正在处理: ${current.meta?.label || current.id}`
: `[${done}/${total}] 等待中...`;
if (statusText !== lastStatus) {
waitSpinner.text = statusText;
lastStatus = statusText;
}
if (done >= total) {
waitSpinner.succeed(`AI 填充完成: ${total} 个维度`);
// 输出各维度结果
if (!opts.json) {
const succeeded = ('tasks' in sessionStatus ? sessionStatus.tasks : []).filter((t) => t.status === 'done').length;
const failed = ('tasks' in sessionStatus ? sessionStatus.tasks : []).filter((t) => t.status === 'error').length;
cli.log(`\n Results: ${succeeded} succeeded, ${failed} failed`);
for (const t of 'tasks' in sessionStatus ? sessionStatus.tasks : []) {
const icon = t.status === 'done' ? '✅' : '❌';
cli.log(` ${icon} ${t.meta?.label || t.id}`);
}
cli.blank();
}
break;
}
}
catch {
// bootstrapTaskManager 可能还没就绪
}
}
}
else if (!opts.json) {
cli.log('');
cli.log(' 📋 下一步:打开 IDE Agent Mode,告诉它「帮我冷启动」');
cli.log(' IDE 会自动调用 MCP 工具完成 AI 分析、提取知识模式、提交候选。');
}
await bootstrap.shutdown();
// 等待 stdout 刷新完成后再退出 (避免管道输出截断)
if (process.stdout.writableLength > 0) {
await new Promise((resolve) => process.stdout.once('drain', resolve));
}
await new Promise((resolve) => setTimeout(resolve, 50)); // 确保管道缓冲区完全刷新
process.exit(0);
}
catch (err) {
cli.error(`\n❌ ${err.message}`);
cli.debug(err.stack);
process.exit(1);
}
});
// ─────────────────────────────────────────────────────
// rescan 命令 (增量知识更新)
// ─────────────────────────────────────────────────────
program
.command('rescan')
.description('增量知识更新:保留已审核 Recipe,清理衍生缓存,重新扫描项目 + AI 补齐')
.option('-d, --dir <path>', '项目目录', '.')
.option('-m, --max-files <n>', '最大扫描文件数', '500')
.option('--dims <ids...>', '仅扫描指定维度(逗号分隔或多次指定)')
.option('--reason <text>', '重扫原因(记录到日志)')
.option('--wait', '等待 AI 异步填充完成(默认骨架完成即退出)')
.option('--json', '以 JSON 格式输出')
.action(async (opts) => {
const projectRoot = resolve(opts.dir);
try {
const { bootstrap, container } = await initContainer({ projectRoot });
const ora = (await import('ora')).default;
const spinner = ora('Rescan: 快照 Recipe → 清理缓存 → Phase 1-4 + 证据审计...').start();
// 直接调用 rescan-internal handler(统一编排管线)
const { rescanInternal } = await import('../lib/external/mcp/handlers/rescan-internal.js');
const logger = container.get('logger');
const raw = await rescanInternal({ container, logger }, {
reason: opts.reason || 'cli-rescan',
dimensions: opts.dims,
skipAsyncFill: !opts.wait,
});
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
const result = parsed?.data || parsed;
spinner.stop();
if (opts.json) {
cli.json(result);
}
else {
cli.log('\n📊 Rescan Report');
cli.log(`${'─'.repeat(50)}`);
const rescan = result.rescan || {};
const audit = result.relevanceAudit || {};
const gap = result.gapAnalysis || {};
cli.log(` 保留 Recipe: ${rescan.preservedRecipes ?? '?'}`);
cli.log(` 扫描文件: ${result.files ?? '?'}`);
cli.log(` 维度: ${gap.totalDimensions ?? '?'} (gap: ${gap.gapDimensions ?? 0})`);
cli.log('\n 证据审计:');
cli.log(` 健康: ${audit.healthy ?? '?'} 观察: ${audit.watch ?? '?'}`);
cli.log(` 衰退: ${audit.decay ?? '?'} 严重: ${audit.severe ?? '?'} 死亡: ${audit.dead ?? '?'}`);
if (audit.proposalsCreated > 0) {
cli.log(` 创建进化提案: ${audit.proposalsCreated}`);
}
if (audit.immediateDeprecated > 0) {
cli.log(` 即时淘汰: ${audit.immediateDeprecated}`);
}
if (gap.gapDimensions > 0 && opts.wait) {
cli.log(`\n AI 正在异步填充 ${gap.gapDimensions} 个 gap 维度...`);
}
else if (gap.gapDimensions > 0) {
cli.log(`\n ${gap.gapDimensions} 个 gap 维度可通过 --wait 等待 AI 填充`);
}
else {
cli.log('\n 所有维度已完全覆盖,无需 AI 补齐。');
}
}
// --wait 模式: 轮询 BootstrapTaskManager
if (opts.wait && result.asyncFill) {
const ora2 = (await import('ora')).default;
const waitSpinner = ora2('AI 正在逐维度填充知识...').start();
let lastStatus = '';
let attempts = 0;
const maxAttempts = Infinity; // 不限时——增量扫描本身就耗时较长
while (attempts < maxAttempts) {
await new Promise((r) => setTimeout(r, 1000));
attempts++;
try {
const taskManager = container.get('bootstrapTaskManager');
const sessionStatus = taskManager.getSessionStatus();
if (!sessionStatus || !('tasks' in sessionStatus)) {
break;
}
const total = sessionStatus.tasks.length;
const done = sessionStatus.tasks.filter((t) => t.status === 'done' || t.status === 'error').length;
const current = sessionStatus.tasks.find((t) => t.status === 'running');
const statusText = current
? `[${done}/${total}] 正在处理: ${current.meta?.label || current.id}`
: `[${done}/${total}] 等待中...`;
if (statusText !== lastStatus) {
waitSpinner.text = statusText;
lastStatus = statusText;
}
if (done >= total) {
waitSpinner.succeed(`AI 填充完成: ${total} 个维度`);
if (!opts.json) {
const succeeded = sessionStatus.tasks.filter((t) => t.status === 'done').length;
const failed = sessionStatus.tasks.filter((t) => t.status === 'error').length;
cli.log(`\n Results: ${succeeded} succeeded, ${failed} failed`);
for (const t of sessionStatus.tasks) {
const icon = t.status === 'done' ? '✅' : '❌';
cli.log(` ${icon} ${t.meta?.label || t.id}`);
}
cli.blank();
}
break;
}
}
catch {
/* bootstrapTaskManager 可能还没就绪 */
}
}
}
await bootstrap.shutdown();
if (process.stdout.writableLength > 0) {
await new Promise((resolve) => process.stdout.once('drain', resolve));
}
await new Promise((resolve) => setTimeout(resolve, 50));
process.exit(0);
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
cli.error(`\n❌ ${msg}`);
if (err instanceof Error && err.stack) {
cli.debug(err.stack);
}
process.exit(1);
}
});
// ─────────────────────────────────────────────────────
// ais 命令 (AI Scan)
// ─────────────────────────────────────────────────────
program
.command('ais [target]')
.description('AI 扫描 Target 源码 → 提取并发布 Recipes(需配置 AI Provider)')
.option('-d, --dir <path>', '项目目录', '.')
.option('-m, --max-files <n>', '最大扫描文件数', '200')
.option('--dry-run', '仅预览,不发布 Recipe')
.option('--json', '以 JSON 格式输出')
.action(async (target, opts) => {
const projectRoot = resolve(opts.dir);
if (target) {
cli.log(`Target: ${target}`);
}
if (opts.dryRun) {
cli.log('ℹ️ Dry-run mode: no Recipes will be published');
}
try {
const { bootstrap, container } = await initContainer({ projectRoot });
const { AiScanService } = await import('../lib/cli/AiScanService.js');
const scanner = new AiScanService({ container, projectRoot });
const ora = (await import('ora')).default;
const spinner = ora('正在扫描源文件并提取 Recipe...').start();
const report = await scanner.scan(target || null, {
maxFiles: parseInt(opts.maxFiles, 10),
dryRun: opts.dryRun,
});
spinner.stop();
if (opts.json) {
cli.json(report);
}
else {
cli.log(`\n📝 AI Scan Report`);
cli.log(` Files scanned: ${report.files}`);
cli.log(` Published: ${report.published}`);
cli.log(` Skipped: ${report.skipped || 0}`);
if (report.errors.length > 0) {
cli.log(` Errors: ${report.errors.length}`);
for (const err of report.errors.slice(0, 10)) {
cli.log(` ❌ ${err}`);
}
if (report.errors.length > 10) {
cli.log(` ... and ${report.errors.length - 10} more`);
}
}
if (!opts.dryRun && report.published > 0) {
cli.log(`\n ✅ ${report.published} Recipes published successfully.`);
}
cli.blank();
}
await bootstrap.shutdown();
}
catch (err) {
cli.error(`\n❌ ${err.message}`);
cli.debug(err.stack);
process.exit(1);
}
});
// ─────────────────────────────────────────────────────
// search 命令
// ─────────────────────────────────────────────────────
program
.command('search <query>')
.description('搜索知识库')
.option('-t, --type <type>', '搜索类型: all, recipe, solution, rule', 'all')
.option('-m, --mode <mode>', '搜索模式: keyword, weighted, semantic, auto', 'auto')
.option('-l, --limit <n>', '结果数量', '10')
.option('-r, --rank', '启用排序管线 (CoarseRanker + MultiSignalRanker)')
.option('-o, --output <format>', '输出格式: text, json', 'text')
.action(async (query, opts) => {
try {
const { bootstrap, container } = await initContainer();
const engine = container.get('searchEngine');
const results = await engine.search(query, {
type: opts.type,
mode: opts.mode,
limit: parseInt(opts.limit, 10),
rank: opts.rank || false,
});
if (opts.output === 'json') {
cli.log(JSON.stringify(results, null, 2));
}
else if (results.items.length === 0) {
cli.log('No results found.');
}
else {
const modeInfo = results.mode || opts.mode;
const rankInfo = results.ranked ? ', ranked' : '';
cli.log(`\n🔍 ${results.items.length} result(s) for "${query}" [mode: ${modeInfo}${rankInfo}]\n`);
for (const item of results.items) {
const badge = item.type === 'recipe' ? '📘' : item.type === 'solution' ? '💡' : '🛡️';
const score = item.score ? ` [${(item.score * 100).toFixed(0)}%]` : '';
cli.log(` ${badge} ${item.title || item.trigger || item.id}${score}`);
if (item.description) {
cli.log(` ${item.description.slice(0, 100)}`);
}
}
cli.blank();
}
await bootstrap.shutdown();
}
catch (err) {
cli.error(`Error: ${err.message}`);
process.exit(1);
}
});
// ─────────────────────────────────────────────────────
// guard 命令
// ─────────────────────────────────────────────────────
program
.command('guard <file>')
.description('对文件运行 Guard 规则检查')
.option('-s, --scope <scope>', '审查维度: file, target, project', 'file')
.option('--json', '以 JSON 格式输出')
.action(async (file, opts) => {
try {
const filePath = resolve(file);
if (!existsSync(filePath)) {
cli.error(`File not found: ${filePath}`);
process.exit(1);
}
const code = readFileSync(filePath, 'utf8');
const { bootstrap, container } = await initContainer();
const { detectLanguage } = await import('../lib/service/guard/GuardCheckEngine.js');
const engine = container.get('guardCheckEngine');
const language = detectLanguage(filePath);
const violations = engine.checkCode(code, language, { scope: opts.scope });
if (opts.json) {
cli.json({
violations,
summary: {
total: violations.length,
errors: violations.filter((v) => v.severity === 'error').length,
warnings: violations.filter((v) => v.severity === 'warning').length,
},
});
}
else if (violations.length === 0) {
cli.log('✅ No violations found.');
}
else {
const errors = violations.filter((v) => v.severity === 'error');
const warnings = violations.filter((v) => v.severity === 'warning');
cli.log(`\n🔍 Guard: ${violations.length} violation(s) — ${errors.length} error(s), ${warnings.length} warning(s)\n`);
for (const v of violations) {
const icon = v.severity === 'error' ? '❌' : v.severity === 'warning' ? '⚠️' : 'ℹ️';
cli.log(` ${icon} [${v.ruleId}] ${v.message}`);
if (v.line) {
cli.log(` Line ${v.line}: ${v.snippet || ''}`);
}
if (v.fixSuggestion) {
cli.log(` 💡 Fix: ${v.fixSuggestion}`);
}
}
cli.blank();
}
await bootstrap.shutdown();
process.exit(violations.some((v) => v.severity === 'error') ? 1 : 0);
}
catch (err) {
cli.error(`Error: ${err.message}`);
process.exit(1);
}
});
// ─────────────────────────────────────────────────────
// guard:ci 命令
// ─────────────────────────────────────────────────────
program
.command('guard:ci [path]')
.description('CI/CD 模式运行全项目 Guard 检查')
.option('--fail-on-error', '有 error 级违规时 exit 1', true)
.option('--fail-on-warning', '超过 warning 阈值时 exit 2')
.option('--max-warnings <n>', 'warning 阈值', '20')
.option('--max-uncertain <n>', 'uncertain 条目阈值 (超出时 exit 2)', '50')
.option('--min-coverage <n>', '最低覆盖率 (0-100,低于时 exit 3)', '0')
.option('--report <format>', '报告格式: json | text | markdown', 'text')
.option('--output <file>', '报告输出文件')
.option('--min-score <n>', 'Quality Gate 最低分', '70')
.option('--max-files <n>', '最大扫描文件数', '500')
.action(async (scanPath, opts) => {
try {
const projectRoot = resolve(scanPath || '.');
const { bootstrap, container } = await initContainer({ projectRoot });
const reporter = container.get('complianceReporter');
const report = await reporter.generate(projectRoot, {
qualityGate: {
maxErrors: 0,
maxWarnings: parseInt(opts.maxWarnings, 10),
minScore: parseInt(opts.minScore, 10),
},
maxFiles: parseInt(opts.maxFiles, 10),
});
// 输出报告
if (opts.report === 'json') {
const output = JSON.stringify(report, null, 2);
if (opts.output) {
const { writeFileSync } = await import('node:fs');
writeFileSync(opts.output, output, 'utf8');
cli.log(`Report written to ${opts.output}`);
}
else {
cli.log(output);
}
}
else {
reporter.printReport(report, { format: opts.report });
}
// 如果也要写文件(非 JSON 格式)
if (opts.output && opts.report !== 'json') {
const { writeFileSync } = await import('node:fs');
writeFileSync(opts.output, JSON.stringify(report, null, 2), 'utf8');
}
await bootstrap.shutdown();
// Exit code: 0=PASS, 1=FAIL(violations), 2=WARN(uncertain/warnings), 3=FAIL(coverage)
const maxUncertain = parseInt(opts.maxUncertain, 10);
const minCoverage = parseInt(opts.minCoverage, 10);
if (report.qualityGate.status === 'FAIL') {
process.exit(report.summary.errors > 0 ? 1 : 2);
}
if (minCoverage > 0 && (report.coverageScore ?? 100) < minCoverage) {
process.exit(3);
}
if (maxUncertain > 0 && (report.uncertainSummary?.total ?? 0) > maxUncertain) {
process.exit(2);
}
process.exit(0);
}
catch (err) {
cli.error(`Error: ${err.message}`);
cli.debug(err.stack);
process.exit(1);
}
});
// ─────────────────────────────────────────────────────
// guard:staged 命令
// ─────────────────────────────────────────────────────
program
.command('guard:staged')
.description('检查 git staged 文件')
.option('--fail-on-error', '有 error 时 exit 1', true)
.option('--json', '以 JSON 格式输出')
.action(async (opts) => {
try {
const { execSync } = await import('node:child_process');
// 获取 staged 文件列表
let stagedFiles;
try {
stagedFiles = execSync('git diff --cached --name-only --diff-filter=ACM', {
encoding: 'utf8',
})
.trim()
.split('\n')
.filter(Boolean);
}
catch (_err) {
cli.error('❌ 无法获取 git staged 文件(是否在 git 仓库中?)');
process.exit(1);
}
if (stagedFiles.length === 0) {
process.exit(0);
}
// 过滤源文件
const { SOURCE_EXTS } = await import('../lib/service/guard/SourceFileCollector.js');
const { extname: _extname } = await import('node:path');
const sourceFiles = stagedFiles.filter((f) => SOURCE_EXTS.has(_extname(f).toLowerCase()));
if (sourceFiles.length === 0) {
process.exit(0);
}
const { bootstrap, container } = await initContainer();
const engine = container.get('guardCheckEngine');
const { detectLanguage: _detectLanguage } = await import('../lib/service/guard/GuardCheckEngine.js');
// 读取文件内容并检查
const files = [];
for (const f of sourceFiles) {
const filePath = resolve(f);
if (existsSync(filePath)) {
files.push({ path: filePath, content: readFileSync(filePath, 'utf8') });
}
}
const result = engine.auditFiles(files, { scope: 'file' });
const { summary } = result;
if (opts.json) {
cli.json({ files: result.files, summary });
}
else if (summary.totalViolations === 0) {
cli.log(`✅ ${sourceFiles.length} staged file(s) checked — no violations.`);
}
else {
cli.log(`\n🔍 Guard (staged): ${summary.totalViolations} violation(s) in ${sourceFiles.length} file(s)\n`);
const filesWithIssues = result.files.filter((f) => f.summary.total > 0);
for (const file of filesWithIssues.slice(0, 10)) {
cli.log(` 📄 ${file.filePath}`);
for (const v of file.violations.slice(0, 5)) {
const icon = v.severity === 'error' ? '❌' : '⚠️';
cli.log(` ${icon} [${v.ruleId}] ${v.message}`);
}
if (file.violations.length > 5) {
cli.log(` ... and ${file.violations.length - 5} more`);
}
}
cli.blank();
}
await bootstrap.shutdown();
process.exit(summary.totalErrors > 0 ? 1 : 0);
}
catch (err) {
cli.error(`Error: ${err.message}`);
cli.debug(err.stack);
process.exit(1);
}
});
// ─────────────────────────────────────────────────────
// panorama 命令
// ─────────────────────────────────────────────────────
program
.command('panorama [path]')
.description('项目全景分析:架构层级、覆盖率、知识空白')
.option('--json', '以 JSON 格式输出')
.option('--gaps', '仅显示知识空白区')
.option('--health', '仅显示健康度评分')
.action(async (scanPath, opts) => {
try {
const projectRoot = resolve(scanPath || '.');
const { bootstrap, container } = await initContainer({ projectRoot });
const panoramaService = container.get('panoramaService');
await panoramaService.ensureData();
if (opts.gaps) {
const gaps = await panoramaService.getGaps();
if (opts.json) {
cli.log(JSON.stringify(gaps, null, 2));
}
else {
cli.log(`\n🔍 Knowledge Gaps: ${gaps.length} found\n`);
for (const g of gaps.slice(0, 20)) {
const priority = g.priority === 'high' ? '🔴' : g.priority === 'medium' ? '🟡' : '🔵';
cli.log(` ${priority} [${g.dimensionName}] ${g.recipeCount} recipes (${g.status}) — ${g.suggestedTopics.join(', ')}`);
}
if (gaps.length > 20) {
cli.log(`\n ... and ${gaps.length - 20} more gaps`);
}
}
await bootstrap.shutdown();
return;
}
if (opts.health) {
const health = await panoramaService.getHealth();
if (opts.json) {
cli.log(JSON.stringify(health, null, 2));
}
else {
const icon = health.healthScore >= 80 ? '✅' : health.healthScore >= 50 ? '⚠️' : '❌';
cli.log(`\n${icon} Panorama Health: ${health.healthScore}/100\n`);
cli.log(` Dimension Coverage: ${health.healthRadar.dimensionCoverage}%`);
cli.log(` Avg Coupling: ${health.avgCoupling}`);
cli.log(` Modules: ${health.moduleCount}`);
cli.log(` Cycles: ${health.cycleCount}`);
cli.log(` Gaps: ${health.gapCount} (${health.highPriorityGaps} high-priority)`);
}
await bootstrap.shutdown();
return;
}
// 默认: 全景概览
const overview = await panoramaService.getOverview();
if (opts.json) {
cli.log(JSON.stringify(overview, null, 2));
}
else {
cli.log(`\n📐 Panorama Overview\n`);
cli.log(` Project: ${overview.projectRoot}`);
cli.log(` Modules: ${overview.moduleCount}`);
cli.log(` Layers: ${overview.layerCount}`);
cli.log(` Files: ${overview.totalFiles}`);
cli.log(` Recipes: ${overview.totalRecipes}`);
cli.log(` Coverage: ${overview.overallCoverage}%`);
cli.log(` Cycles: ${overview.cycleCount}`);
cli.log(` Gaps: ${overview.gapCount}`);
if (overview.layers && overview.layers.length > 0) {
cli.log(`\n Layers:`);
for (const layer of overview.layers) {
const totalFiles = layer.modules.reduce((sum, m) => sum + m.fileCount, 0);
cli.log(` ${layer.name}: ${layer.modules.length} modules, ${totalFiles} files`);
}
}
}
await bootstrap.shutdown();
}
catch (err) {
cli.error(`Error: ${err.message}`);
cli.debug(err.stack);
process.exit(1);
}
});
// ─────────────────────────────────────────────────────
// server 命令
// ─────────────────────────────────────────────────────
program
.command('server')
.description('启动 API 服务器')
.option('-p, --port <port>', '端口', '3000')
.option('-H, --host <host>', '绑定地址', '127.0.0.1')
.action(async (opts) => {
// 设置环境变量后启动 api-server
process.env.PORT = opts.port;
process.env.HOST = opts.host;
await import('./api-server.js');
});
// ─────────────────────────────────────────────────────
// ui 命令 (Dashboard)
// ─────────────────────────────────────────────────────
program
.command('ui')
.description('启动 Dashboard UI(API 服务 + 前端开发服务器)')
.option('-p, --port <port>', 'API 服务端口', '3000')
.option('--no-open', '禁止自动打开浏览器(CI/CD 环境适用)')
.option('-d, --dir <directory>', '指定 AutoSnippet 项目目录(默认:当前目录)')
.option('--api-only', '仅启动 API 服务(不启动前端)')
.action(async (opts) => {
const { spawn } = await import('node:child_process');
// 标记为长驻 API 服务进程(CacheCoordinator 用于判断是否启动轮询)
process.env.ASD_API_SERVER = '1';
// 项目根目录:-d 选项 > 环境变量 ASD_CWD > 当前目录
const projectRoot = opts.dir || process.env.ASD_CWD || process.cwd();
const port = opts.port;
const host = '127.0.0.1';
process.env.PORT = port;
process.env.HOST = host;
let httpServer;
try {
const { default: HttpServer } = await import('../lib/http/HttpServer.js');
const { container } = await initContainer({ projectRoot });
// 连接 EventBus → Gateway(供 SignalCollector 监听事件)
try {
const eventBus = container.get('eventBus');
const gateway = container.get('gateway');
gateway.eventBus = eventBus;
}
catch {
/* EventBus 不可用不阻塞启动 */
}
httpServer = new HttpServer({ port, host });
await httpServer.initialize();
await httpServer.start();
// ── UiStartupTasks: 后台异步刷新(不阻塞 UI) ──
import('../lib/service/bootstrap/UiStartupTasks.js')
.then(({ runUiStartupTasks }) => runUiStartupTasks({ projectRoot, container }))
.then((report) => {
if (report.errors.length > 0) {
cli.warn(`⚠️ UiStartupTasks completed with ${report.errors.length} error(s)`);
}
})
.catch((err) => {
cli.debug(`UiStartupTasks failed: ${err.message}`);
});
// ── MCP 配置检测 ──
const cursorMcpPath = join(projectRoot, '.cursor', 'mcp.json');
const vscodeMcpPath = join(projectRoot, '.vscode', 'mcp.json');
const hasMcpConfig = (() => {
try {
const c = JSON.parse(readFileSync(cursorMcpPath, 'utf8'));
if ('autosnippet' in (c.mcpServers || {})) {
return true;
}
}
catch {
/* */
}
try {
const v = JSON.parse(readFileSync(vscodeMcpPath, 'utf8'));
if ('autosnippet' in (v.servers || {})) {
return true;
}
}
catch {
/* */
}
return false;
})();
if (hasMcpConfig) {
console.log('💡 请确认 IDE 中 AutoSnippet MCP 开关已打开,否则 Agent 无法调用工具');
}
// 启动 SignalCollector 后台 AI 分析服务
try {
const { SignalCollector } = await import('../lib/service/skills/SignalCollector.js');
const { getRealtimeService } = await import('../lib/infrastructure/realtime/RealtimeService.js');
const db = container.get('database');
const agentFactory = container.get('agentFactory');
const knowledgeRepo = container.get('knowledgeRepository');
const auditRepo = container.get('auditRepository');
const signalCollector = new SignalCollector({
projectRoot,
knowledgeRepo: knowledgeRepo,
auditRepo: auditRepo,
agentFactory,
container,
mode: process.env.ASD_SIGNAL_MODE || 'auto',
intervalMs: parseInt(process.env.ASD_SIGNAL_INTERVAL || '3600000', 10),
onSuggestions: (suggestions) => {
try {
const realtime = getRealtimeService();
realtime.broadcastEvent('skill:suggestions', { suggestions });
}
catch {
/* realtime 未就绪 */
}
},
});
signalCollector.start();
globalThis._signalCollector = signalCollector;
// 将 SignalCollector 绑定到 AIRecallStrategy (延迟注入)
try {
const aiStrategy = container.singletons._aiRecallStrategy;
if (aiStrategy && typeof aiStrategy.setSignalCollector === 'function') {
aiStrategy.setSignalCollector(signalCollector);
}
}
catch {
/* recommendation pipeline not yet initialized */
}
}
catch (scErr) {
cli.warn(`⚠️ SignalCollector failed to start: ${scErr.message}`);
cli.debug(scErr.stack);
}
if (opts.apiOnly) {
return;
}
// 2. 启动 Dashboard UI
const dashboardDir = DASHBOARD_DIR;
const distDir = join(dashboardDir, 'dist');
const hasPrebuilt = existsSync(join(distDir, 'index.html'));
const hasSrc = existsSync(join(dashboardDir, 'src'));
if (hasPrebuilt && !hasSrc) {
// ── 生产模式:npm 安装的包,在 API 服务器上直接托管预构建产物 ──
// 同端口同 origin → /api 路由自然可达,无跨域问题
httpServer.mountDashboard(distDir);
const dashUrl = `http://127.0.0.1:${port}/`;
console.log(`\n 🚀 Dashboard: ${dashUrl}\n`);
if (opts.open !== false) {
const open = (await import('open')).default;
open(dashUrl);
}
}
else {
// ── 开发模式:有源码,启动 Vite Dev Server ──
if (!existsSync(join(dashboardDir, 'node_modules'))) {
const install = spawn('npm', ['install'], { cwd: dashboardDir, stdio: 'inherit' });
await new Promise((resolve, reject) => {
install.on('close', (code) => code === 0 ? resolve(undefined) : reject(new Error(`npm install exited with ${code}`)));
});
}
const viteArgs = ['--host'];
if (opts.open !== false) {
viteArgs.push('--open');
}
const vite = spawn('npx', ['vite', ...viteArgs], {
cwd: dashboardDir,
stdio: 'inherit',
env: { ...process.env, VITE_API_URL: `http://127.0.0.1:${port}` },
});
vite.on('error', (err) => {
cli.error(`❌ Vite failed to start: ${err.message}`);
});
process.on('SIGINT', () => {
vite.kill();
process.exit(0);
});
}
}
catch (err) {
cli.error(`❌ API server failed to start: ${err.message}`);
if (err.code === 'EADDRINUSE') {
cli.error(` Port ${port} is already in use. Kill it with: lsof -ti:${port} | xargs kill -9`);
}
process.exit(1);
}
});
// ─────────────────────────────────────────────────────
// status 命令
// ─────────────────────────────────────────────────────
program
.command('status')
.description('检查环境状态')
.option('--json', 'JSON 格式输出')
.action(async (opts) => {
cli.log('\n AutoSnippet Environment Status');
cli.log(` ${'─'.repeat(40)}`);
// AI 配置
const { getAiConfigInfo } = await import('../lib/external/ai/AiFactory.js');
const aiInfo = getAiConfigInfo();
if (aiInfo.provider && aiInfo.provider !== 'none') {
cli.log(` AI Provider: ${aiInfo.provider}`);
if (aiInfo.model) {
cli.log(` AI Model: ${aiInfo.model}`);
}
}
else {
cli.log(' AI Provider: 通过 IDE Agent(无需配置)');
}
// 检查数据库
const dbPath = join(process.cwd(), '.autosnippet', 'autosnippet.db');
const dbExists = existsSync(dbPath);
cli.log(` Database: ${dbExists ? `✅ ${dbPath}` : '❌ not found'}`);
// 检查 .autosnippet 目录
const asdDir = join(process.cwd(), '.autosnippet');
cli.log(` Workspace: ${existsSync(asdDir) ? '✅ .autosnippet/' : '❌ not initialized (run asd setup)'}`);
// 检查依赖
cli.log(' Dependencies:');
for (const dep of ['better-sqlite3', 'commander', 'express']) {
try {
await import(dep);
cli.log(` ✅ ${dep}`);
}
catch {
cli.log(` ❌ ${dep} (missing)`);
}
}
// 如果数据库存在,加载知识库统计
if (dbExists) {
try {
const projectRoot = resolve('.');
const { bootstrap, container } = await initContainer({ projectRoot });
const knowledgeService = container.get('knowledgeService');
const stats = (await knowledgeService.getStats());
if (stats) {
cli.log(' Knowledge:');
cli.log(` Total: ${stats.total ?? 0} Active: ${stats.active ?? 0} Staging: ${stats.staging ?? 0} Evolving: ${stats.evolving ?? 0} Decaying: ${stats.decaying ?? 0} Pending: ${stats.pending ?? 0} Deprecated: ${stats.deprecated ?? 0}`);
cli.log(` Rules: ${stats.rules ?? 0} Patterns: ${stats.patterns ?? 0} Facts: ${stats.facts ?? 0}`);
}
// Signal Bus 统计
const signalBus = container.get('signalBus');
if (signalBus) {
const bus = signalBus;
cli.log(' Signals:');
cli.log(` Emitted: ${bus.emitCount ?? 0} Listeners: ${bus.listenerCount ?? 0}`);
}
await bootstrap.shutdown();
}
catch {
// 降级: 无法加载容器时只展示基础状态
}
}
if (opts.json) {
// 简化 JSON 输出模式
const result = {
aiProvider: aiInfo.provider ?? 'ide-agent',
aiModel: aiInfo.model ?? null,
database: dbExists,
workspace: existsSync(asdDir),
};
cli.json(result);
}
cli.blank();
});
// ─────────────────────────────────────────────────────
// health 命令
// ─────────────────────────────────────────────────────
program
.command('health')
.description('综合健康报告:系统状态、知识生命周期、Guard 合规、信号统计')
.option('-d, --dir <path>', '项目目录', '.')
.option('--json', '以 JSON 格式输出')
.action(async (opts) => {
const projectRoot = resolve(opts.dir);
const { getAiConfigInfo } = await import('../lib/external/ai/AiFactory.js');
const aiInfo = getAiConfigInfo();
const aiOk = !!(aiInfo.provider && aiInfo.provider !== 'none');
const dbPath = join(projectRoot, '.autosnippet', 'autosnippet.db');
const dbExists = existsSync(dbPath);
let dbSizeMB = 0;
let dbEntries = 0;
let guardRuleCount = 0;
let knowledgeStats = {};
let complianceScore = 0;
let coverageScore = 0;
let confidencePct = 0;
let signalEmitted = 0;
let signalListeners = 0;
if (dbExists) {
try {
const { statSync } = await import('node:fs');
const stat = statSync(dbPath);
dbSizeMB = +(stat.size / (1024 * 1024)).toFixed(1);
}
catch {
/* stat 失败不阻塞 */
}
try {
const { bootstrap, container } = await initContainer({ projectRoot });
try {
const knowledgeService = container.get('knowledgeService');
const stats = (await knowledgeService.getStats());
if (stats) {
knowledgeStats = stats;
dbEntries = stats.total ?? 0;
}
}
catch {
/* knowledge service 不可用 */
}
try {
const engine = container.get('guardCheckEngine');
const rules = engine.getRules();
guardRuleCount = rules.length;
}
catch {
/* guard engine 不可用 */
}
try {
const reporter = container.get('complianceReporter');
const report = await reporter.generate(projectRoot, {
qualityGate: { maxErrors: 0, maxWarnings: 100, minScore: 0 },
maxFiles: 200,
});
complianceScore = report.complianceScore ?? 0;
coverageScore = report.coverageScore ?? 0;
confidencePct = report.confidenceScore ?? 0;
}
catch {
/* compliance reporter 不可用 */
}
try {
const signalBus = container.get('signalBus');
signalEmitted = signalBus.emitCount ?? 0;
signalListeners = signalBus.listenerCount ?? 0;
}
catch {
/* signal bus 不可用 */
}
await bootstrap.shutdown();
}
catch {
/* container init 失败,降级展示基础信息 */
}
}
const healthData = {
system: {
ai: aiOk,
db: dbExists,
dbSizeMB,
dbEntries,
guardRules: guardRuleCount,
},
knowledge: {
active: knowledgeStats.active ?? 0,
staging: knowledgeStats.staging ?? 0,
evolving: knowledgeStats.evolving ?? 0,
decaying: knowledgeStats.decaying ?? 0,
},
guard: {
compliance: complianceScore,
coverage: coverageScore,
confidence: confidencePct,
},
signals: {
emitted: signalEmitted,
listeners: signalListeners,
},
};
if (opts.json) {
cli.json(healthData);
}
else {
const dbStatus = dbExists ? `✅(${dbSizeMB}MB, ${dbEntries} entries)` : '❌';
const aiIcon = aiOk ? '✅' : '❌';
cli.log('');
cli.log('AutoSnippet Health Report');
cli.log('═════════════════════════');
cli.log(`🔧 System: AI:${aiIcon} DB:${dbStatus} Guard:${guardRuleCount} rules`);
cli.log(`📊 Knowledge: Active:${healthData.knowledge.active} Staging:${healthData.knowledge.staging} Evolving:${healthData.knowledge.evolving} Decaying:${healthData.knowledge.decaying}`);
cli.log(`🛡️ Guard: Compliance:${complianceScore} Coverage:${coverageScore} Confidence:${confidencePct}%`);
cli.log(`📡 Signals: emitted:${signalEmitted} listeners:${signalListeners}`);
cli.blank();
}
});
// ─────────────────────────────────────────────────────
// embed 命令 — 构建/重建语义向量索引
// ─────────────────────────────────────────────────────
program
.command('embed')
.description('构建/重建语义向量索引(可选 — 增强搜索质量,非必需)')
.option('-d, --dir <path>', '项目目录', '.')
.option('--force', '忽略增量检测,全量重建')
.option('--clear', '清空现有索引后重建')
.option('--dry-run', '只报告不执行')
.option('--json', 'JSON 输出')
.option('--validate', '只验证索引健康状态')
.action(async (opts) => {
const projectRoot = resolve(opts.dir);
const { bootstrap, container } = await initContainer({ projectRoot });
try {
// 优先使用 VectorService,降级到 IndexingPipeline
const hasVectorService = !!container.services.vectorService;
if (opts