#!/usr/bin/env node import { Command } from 'commander'; import { spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; import { basename, join, resolve } from 'node:path'; import { loadApiKey, saveApiKey } from './auth-manager.js'; import { runCombinedScan, type CombinedScanOptions } from './combined-scan.js'; import { SilenceApiError, startScan, uploadReport } from './silence-api.js'; type MinSeverity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; interface CliOptions { verify: boolean; verifyAll: boolean; minSeverity: string; llm: boolean; skipUa: boolean; arch: boolean; security: boolean; open: boolean; unicode: boolean; } const unicodeOutput = !process.argv.includes('--no-unicode'); function getMissingApiKeyMessage(unicode: boolean): string { return [ `${unicode ? '❌' : '[ERROR]'} No API key configured. Run: qwen-s auth `, ' Get a key at: https://supreme.silenceai.net/#pricing', ].join('\n'); } function getInvalidApiKeyMessage(unicode: boolean): string { return `${unicode ? '❌' : '[ERROR]'} Invalid API key. Check your key or run: qwen-s auth `; } function getSubscriptionExpiredMessage(unicode: boolean, upgradeUrl?: string): string { return `${unicode ? '❌' : '[ERROR]'} Subscription expired. Renew at: ${upgradeUrl ?? 'https://supreme.silenceai.net/#pricing'}`; } function toAuthStartError(error: SilenceApiError, unicode: boolean): Error { if (error.kind === 'invalid_key') { return new Error(getInvalidApiKeyMessage(unicode)); } if (error.kind === 'subscription_expired') { return new Error(getSubscriptionExpiredMessage(unicode, error.upgradeUrl)); } return new Error(`${unicode ? '❌' : '[ERROR]'} Failed to authenticate scan start: ${error.message}`); } function toUploadWarning(error: SilenceApiError, unicode: boolean): string { const warnPrefix = unicode ? '⚠️ ' : '[WARN]'; if (error.kind === 'invalid_key') { return `${warnPrefix} Could not upload dashboard report: ${getInvalidApiKeyMessage(unicode).replace(/^❌\s*/, '').replace(/^\[ERROR\]\s*/, '')}`; } if (error.kind === 'subscription_expired') { return `${warnPrefix} Could not upload dashboard report: ${getSubscriptionExpiredMessage(unicode, error.upgradeUrl).replace(/^❌\s*/, '').replace(/^\[ERROR\]\s*/, '')}`; } return `${warnPrefix} Could not upload dashboard report: ${error.message}`; } function resolveMinSeverity(input: string): MinSeverity { const normalized = input.toUpperCase(); if (normalized === 'CRITICAL' || normalized === 'HIGH' || normalized === 'MEDIUM' || normalized === 'LOW') { return normalized; } throw new Error(`Invalid --min-severity value: ${input}. Use CRITICAL|HIGH|MEDIUM|LOW.`); } async function openInBrowser(filePath: string): Promise { const cmd = process.platform === 'win32' ? 'explorer' : process.platform === 'darwin' ? 'open' : 'xdg-open'; await new Promise((resolve, reject) => { const child = spawn(cmd, [filePath], { stdio: 'ignore', detached: false }); child.once('error', reject); child.once('exit', (code) => { if (code === 0) resolve(); else reject(new Error(`${cmd} exited with code ${String(code)}`)); }); }); } async function runDashboard(projectPath: string): Promise { const { getUAInstallStatus } = await import('./ua-installer.js'); const status = getUAInstallStatus(); if (!status.installed) { console.log(`${unicodeOutput ? '❌' : '[ERROR]'} Understand-Anything is not installed.`); console.log('Run: qwen-s install-ua'); return; } const graphPath = resolve(projectPath, '.understand-anything', 'knowledge-graph.json'); if (!existsSync(graphPath)) { console.log(`${unicodeOutput ? '❌' : '[ERROR]'} Knowledge graph not found.`); console.log('Run UA analysis first:'); console.log(` 1. Open Qwen Code: cd ${projectPath} && qwen`); console.log(' 2. Say: запусти скан с /understand'); console.log(' 3. Wait for completion (~20 min)'); console.log(` 4. Then run: qwen-s dashboard ${projectPath}`); return; } const localVite = join(status.dashboardPath, 'node_modules', '.bin', 'vite'); const [cmd, args] = existsSync(localVite) ? [localVite, ['--open']] : ['npx', ['--yes', 'vite', '--open']]; const child = spawn(cmd, args, { cwd: status.dashboardPath, env: { ...process.env, GRAPH_DIR: projectPath }, stdio: 'inherit', detached: true, }); child.unref(); console.log('✓ Dashboard starting at http://localhost:5173'); console.log(`Graph: ${graphPath}`); console.log('Press Ctrl+C to stop.'); } function buildProgram(): Command { const program = new Command(); program .name('qwen-s') .description('Qwen Code S-Edition CLI') .argument('[path]', 'Path to project', process.cwd()) .option('--no-verify', 'Skip Qwen verification') .option('--verify-all', 'Verify all findings regardless of count (uses more API quota)') .option('--min-severity ', 'Minimum severity: CRITICAL|HIGH|MEDIUM|LOW', 'LOW') .option('--no-llm', 'Skip Qwen architecture analysis (graph only)') .option('--skip-ua', 'Use existing knowledge-graph.json (skip UA subprocess)') .option('--no-arch', 'Skip architecture scan') .option('--no-security', 'Skip security scan') .option('--open', 'Open combined report in browser after completion') .option('--no-unicode', 'Use ASCII-only output (for Windows CMD/PowerShell)') .action(async (projectPath: string, rawOptions: CliOptions) => { if (!rawOptions.arch && !rawOptions.security) { throw new Error('Both scanners are disabled. Remove one of --no-arch or --no-security.'); } const options: CombinedScanOptions = { projectPath, verify: rawOptions.verify, includeArch: rawOptions.arch, includeSecurity: rawOptions.security, verifyAll: rawOptions.verifyAll, minSeverity: resolveMinSeverity(rawOptions.minSeverity), llm: rawOptions.llm, skipUa: rawOptions.skipUa, }; const apiKey = loadApiKey(); if (apiKey === null) { throw new Error(getMissingApiKeyMessage(rawOptions.unicode)); } const projectPathResolved = resolve(projectPath); const projectName = basename(projectPathResolved) || projectPathResolved; const scanStartedAt = Date.now(); const combined = await runCombinedScan(options); const elapsedSeconds = Math.max(1, Math.round((Date.now() - scanStartedAt) / 1000)); let scanId: number | null = null; try { const started = await startScan(apiKey, projectName); scanId = started.scanId; } catch (error) { const warnPrefix = rawOptions.unicode ? '⚠️ ' : '[WARN]'; if (error instanceof SilenceApiError) { console.warn(`${warnPrefix} Could not register dashboard scan: ${toAuthStartError(error, rawOptions.unicode).message.replace(/^❌\s*/, '').replace(/^\[ERROR\]\s*/, '')}`); } else { console.warn(`${warnPrefix} Could not register dashboard scan: ${error instanceof Error ? error.message : String(error)}`); } } const criticalFindings = (combined.security as ({ critical?: number } & typeof combined.security) | null)?.critical ?? 0; const highFindings = combined.security?.high ?? 0; const mediumFindings = combined.security?.medium ?? 0; const lowFindings = combined.security?.low ?? 0; const totalFindings = criticalFindings + highFindings + mediumFindings + lowFindings; const dockerfileCriticalFindings = combined.dockerfile?.critical ?? 0; const dockerfileHighFindings = combined.dockerfile?.high ?? 0; const dockerfileMediumFindings = combined.dockerfile?.medium ?? 0; const dockerfileLowFindings = combined.dockerfile?.low ?? 0; console.log(`${rawOptions.unicode ? '✅' : '[OK]'} Scan completed in ${String(elapsedSeconds)}s`); console.log( `${rawOptions.unicode ? '🔍' : '[SCAN]'} Found: ${String(criticalFindings)} CRITICAL · ${String(highFindings)} HIGH · ${String(mediumFindings)} MEDIUM · ${String(lowFindings)} LOW`, ); if (scanId !== null) { try { const uploaded = await uploadReport(apiKey, scanId, combined.reportPath, { totalFindings, criticalFindings, highFindings, mediumFindings, lowFindings, dockerfileCriticalFindings, dockerfileHighFindings, dockerfileMediumFindings, dockerfileLowFindings, }); console.log(`${rawOptions.unicode ? '📊' : '[REPORT]'} Report: ${uploaded.reportUrl}`); } catch (error) { if (error instanceof SilenceApiError) { console.warn(toUploadWarning(error, rawOptions.unicode)); } else { console.warn(`${rawOptions.unicode ? '⚠️ ' : '[WARN]'} Could not upload dashboard report: ${error instanceof Error ? error.message : String(error)}`); } } } if (rawOptions.open) { await openInBrowser(combined.reportPath); } }); program .command('auth ') .description('Save Silence AI scanner API key') .action((key: string) => { saveApiKey(key); console.log(`${unicodeOutput ? '✅' : '[OK]'} API key saved to ~/.qwen-s/config.json`); }); program .command('install-ua') .description('Install or update Understand-Anything plugin') .action(async () => { const { installUA, getUAInstallStatus } = await import('./ua-installer.js'); const status = getUAInstallStatus(); if (status.installed) { console.log('✓ Understand-Anything already installed.'); console.log(` Path: ${status.path}`); return; } await installUA(); }); program .command('dashboard') .description('Launch interactive knowledge graph dashboard') .option('--path ', 'Path to project', process.cwd()) .action(async (opts: { path: string }) => { await runDashboard(resolve(opts.path)); }); return program; } async function main(): Promise { const program = buildProgram(); await program.parseAsync(process.argv); } main().catch((error: unknown) => { if (error instanceof Error) { console.error(error.message); } else { console.error(String(error)); } process.exitCode = 1; });