pqm-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +254 -0
  2. package/bin/pqm.js +6 -0
  3. package/package.json +31 -0
  4. package/src/ai/analyzer/collector.js +191 -0
  5. package/src/ai/analyzer/dependency.js +269 -0
  6. package/src/ai/analyzer/index.js +234 -0
  7. package/src/ai/analyzer/quality.js +241 -0
  8. package/src/ai/analyzer/security.js +302 -0
  9. package/src/ai/index.js +16 -0
  10. package/src/ai/providers/bailian.js +121 -0
  11. package/src/ai/providers/base.js +177 -0
  12. package/src/ai/providers/deepseek.js +100 -0
  13. package/src/ai/providers/index.js +100 -0
  14. package/src/ai/providers/openai.js +100 -0
  15. package/src/builders/base.js +35 -0
  16. package/src/builders/rollup.js +47 -0
  17. package/src/builders/vite.js +47 -0
  18. package/src/cli.js +41 -0
  19. package/src/commands/ai.js +317 -0
  20. package/src/commands/build.js +24 -0
  21. package/src/commands/commit.js +68 -0
  22. package/src/commands/config.js +113 -0
  23. package/src/commands/doctor.js +146 -0
  24. package/src/commands/init.js +61 -0
  25. package/src/commands/login.js +37 -0
  26. package/src/commands/publish.js +250 -0
  27. package/src/commands/release.js +107 -0
  28. package/src/commands/scan.js +239 -0
  29. package/src/commands/status.js +129 -0
  30. package/src/commands/watch.js +170 -0
  31. package/src/commands/webhook.js +240 -0
  32. package/src/config/detector.js +82 -0
  33. package/src/config/global.js +136 -0
  34. package/src/config/loader.js +49 -0
  35. package/src/core/builder.js +88 -0
  36. package/src/index.js +5 -0
  37. package/src/logs/build.js +47 -0
  38. package/src/logs/manager.js +60 -0
  39. package/src/report/formatter.js +282 -0
  40. package/src/utils/http.js +130 -0
  41. package/src/utils/logger.js +24 -0
  42. package/src/utils/prompt.js +132 -0
  43. package/src/utils/spinner.js +134 -0
@@ -0,0 +1,136 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ export const CONFIG_DIR = path.join(os.homedir(), '.pqm');
6
+ export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ const DEFAULT_CONFIG = {
9
+ ai: {
10
+ provider: 'bailian',
11
+ apiKey: '',
12
+ model: 'qwen-turbo',
13
+ enabled: true
14
+ },
15
+ scan: {
16
+ exclude: ['node_modules', 'dist', '.git', '**/*.min.js', '**/*.map'],
17
+ maxFileSize: 1048576 // 1MB
18
+ }
19
+ };
20
+
21
+ /**
22
+ * Ensure config directory exists
23
+ */
24
+ function ensureConfigDir() {
25
+ if (!fs.existsSync(CONFIG_DIR)) {
26
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Load global configuration
32
+ * @returns {Object} Configuration object
33
+ */
34
+ export function loadGlobalConfig() {
35
+ ensureConfigDir();
36
+
37
+ if (fs.existsSync(CONFIG_FILE)) {
38
+ try {
39
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
40
+ const userConfig = JSON.parse(content);
41
+ return { ...DEFAULT_CONFIG, ...userConfig };
42
+ } catch (err) {
43
+ // Return defaults if config is corrupted
44
+ return { ...DEFAULT_CONFIG };
45
+ }
46
+ }
47
+
48
+ return { ...DEFAULT_CONFIG };
49
+ }
50
+
51
+ /**
52
+ * Save global configuration
53
+ * @param {Object} config - Configuration object to save
54
+ */
55
+ export function saveGlobalConfig(config) {
56
+ ensureConfigDir();
57
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
58
+ }
59
+
60
+ /**
61
+ * Get a nested configuration value
62
+ * @param {string} key - Dot-separated key path (e.g., 'ai.provider')
63
+ * @returns {*} Configuration value
64
+ */
65
+ export function getGlobalConfigValue(key) {
66
+ const config = loadGlobalConfig();
67
+ const keys = key.split('.');
68
+ let result = config;
69
+
70
+ for (const k of keys) {
71
+ result = result?.[k];
72
+ if (result === undefined) break;
73
+ }
74
+
75
+ return result;
76
+ }
77
+
78
+ /**
79
+ * Set a nested configuration value
80
+ * @param {string} key - Dot-separated key path
81
+ * @param {*} value - Value to set
82
+ */
83
+ export function setGlobalConfigValue(key, value) {
84
+ const config = loadGlobalConfig();
85
+ const keys = key.split('.');
86
+ let current = config;
87
+
88
+ for (let i = 0; i < keys.length - 1; i++) {
89
+ if (!current[keys[i]]) {
90
+ current[keys[i]] = {};
91
+ }
92
+ current = current[keys[i]];
93
+ }
94
+
95
+ current[keys[keys.length - 1]] = value;
96
+ saveGlobalConfig(config);
97
+ }
98
+
99
+ /**
100
+ * Update AI configuration
101
+ * @param {Object} aiConfig - AI configuration object
102
+ */
103
+ export function updateAIConfig(aiConfig) {
104
+ const config = loadGlobalConfig();
105
+ config.ai = { ...config.ai, ...aiConfig };
106
+ saveGlobalConfig(config);
107
+ }
108
+
109
+ /**
110
+ * Get AI configuration
111
+ * @returns {Object} AI configuration
112
+ */
113
+ export function getAIConfig() {
114
+ return loadGlobalConfig().ai;
115
+ }
116
+
117
+ /**
118
+ * Check if AI is configured
119
+ * @returns {boolean} True if API key is set
120
+ */
121
+ export function isAIConfigured() {
122
+ const aiConfig = getAIConfig();
123
+ return !!(aiConfig && aiConfig.apiKey);
124
+ }
125
+
126
+ export default {
127
+ loadGlobalConfig,
128
+ saveGlobalConfig,
129
+ getGlobalConfigValue,
130
+ setGlobalConfigValue,
131
+ updateAIConfig,
132
+ getAIConfig,
133
+ isAIConfigured,
134
+ CONFIG_DIR,
135
+ CONFIG_FILE
136
+ };
@@ -0,0 +1,49 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const CONFIG_FILE = '.pqmrc';
5
+
6
+ const DEFAULT_CONFIG = {
7
+ root: './src',
8
+ exclude: ['node_modules', 'dist', '.git', '**/*.test.js', '**/*.spec.js'],
9
+ buildTool: 'auto',
10
+ buildMode: 'incremental',
11
+ buildOnStart: true,
12
+ webhook: {
13
+ enabled: false,
14
+ port: 3200
15
+ },
16
+ log: {
17
+ level: 'info',
18
+ file: '.pqm/pqm.log'
19
+ }
20
+ };
21
+
22
+ export function loadConfig() {
23
+ const configPath = path.resolve(process.cwd(), CONFIG_FILE);
24
+
25
+ if (fs.existsSync(configPath)) {
26
+ try {
27
+ const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
28
+ return { ...DEFAULT_CONFIG, ...userConfig };
29
+ } catch (err) {
30
+ console.warn('Failed to parse .pqmrc, using defaults');
31
+ }
32
+ }
33
+
34
+ return DEFAULT_CONFIG;
35
+ }
36
+
37
+ export function detectProjectType() {
38
+ const cwd = process.cwd();
39
+ const indicators = {
40
+ typescript: fs.existsSync(path.join(cwd, 'tsconfig.json')),
41
+ vite: fs.existsSync(path.join(cwd, 'vite.config.js')) ||
42
+ fs.existsSync(path.join(cwd, 'vite.config.ts')),
43
+ rollup: fs.existsSync(path.join(cwd, 'rollup.config.js')) ||
44
+ fs.existsSync(path.join(cwd, 'rollup.config.mjs')),
45
+ node: fs.existsSync(path.join(cwd, 'package.json'))
46
+ };
47
+
48
+ return indicators;
49
+ }
@@ -0,0 +1,88 @@
1
+ import { spawn } from 'child_process';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { logger } from '../utils/logger.js';
5
+
6
+ export async function buildProject(options = {}) {
7
+ const { tool = 'auto', mode = 'production' } = options;
8
+
9
+ // Detect build tool
10
+ const buildTool = tool === 'auto' ? detectBuildTool() : tool;
11
+
12
+ if (!buildTool) {
13
+ throw new Error('No build tool detected. Please install vite or rollup.');
14
+ }
15
+
16
+ logger.info(`Using ${buildTool} for build (${mode})`);
17
+
18
+ switch (buildTool) {
19
+ case 'vite':
20
+ return runViteBuild(mode);
21
+ case 'rollup':
22
+ return runRollupBuild(mode);
23
+ default:
24
+ throw new Error(`Unknown build tool: ${buildTool}`);
25
+ }
26
+ }
27
+
28
+ function detectBuildTool() {
29
+ const cwd = process.cwd();
30
+
31
+ // Check for vite config
32
+ if (existsSync(join(cwd, 'vite.config.js')) ||
33
+ existsSync(join(cwd, 'vite.config.ts'))) {
34
+ return 'vite';
35
+ }
36
+
37
+ // Check for rollup config
38
+ if (existsSync(join(cwd, 'rollup.config.js')) ||
39
+ existsSync(join(cwd, 'rollup.config.mjs'))) {
40
+ return 'rollup';
41
+ }
42
+
43
+ // Check package.json for vite/rollup
44
+ try {
45
+ const pkgPath = join(cwd, 'package.json');
46
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
47
+ if (pkg.devDependencies?.vite || pkg.dependencies?.vite) {
48
+ return 'vite';
49
+ }
50
+ if (pkg.devDependencies?.rollup || pkg.dependencies?.rollup) {
51
+ return 'rollup';
52
+ }
53
+ } catch {
54
+ // Ignore
55
+ }
56
+
57
+ return null;
58
+ }
59
+
60
+ function runViteBuild(mode) {
61
+ return runCommand('npx', ['vite', 'build', '--mode', mode === 'production' ? 'production' : 'development']);
62
+ }
63
+
64
+ function runRollupBuild(mode) {
65
+ return runCommand('npx', ['rollup', '-c']);
66
+ }
67
+
68
+ function runCommand(cmd, args) {
69
+ return new Promise((resolve, reject) => {
70
+ const child = spawn(cmd, args, {
71
+ cwd: process.cwd(),
72
+ stdio: 'inherit',
73
+ shell: true
74
+ });
75
+
76
+ child.on('close', (code) => {
77
+ if (code === 0) {
78
+ resolve();
79
+ } else {
80
+ reject(new Error(`Build failed with code ${code}`));
81
+ }
82
+ });
83
+
84
+ child.on('error', (err) => {
85
+ reject(err);
86
+ });
87
+ });
88
+ }
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // Core module exports
2
+ export { loadConfig } from './config/loader.js';
3
+ export { detectProjectConfig, autoDetectConfig } from './config/detector.js';
4
+ export { buildProject } from './core/builder.js';
5
+ export { logger, formatTimestamp, logWithTime } from './utils/logger.js';
@@ -0,0 +1,47 @@
1
+ import LogManager from './manager.js';
2
+
3
+ /**
4
+ * Logger class for building operations
5
+ */
6
+ export class BuildLogger extends LogManager {
7
+ constructor(logFile) {
8
+ super(logFile);
9
+ }
10
+
11
+ buildStart(options) {
12
+ this.info('build:start', {
13
+ tool: options.tool,
14
+ mode: options.mode,
15
+ files: options.files
16
+ });
17
+ }
18
+
19
+ buildEnd(result) {
20
+ this.info('build:end', {
21
+ success: result.success,
22
+ duration: result.duration,
23
+ outputFiles: result.outputFiles
24
+ });
25
+ }
26
+
27
+ buildError(error) {
28
+ this.error('build:error', {
29
+ message: error.message,
30
+ stack: error.stack
31
+ });
32
+ }
33
+
34
+ fileChange(event, path) {
35
+ this.info('file:change', { event, path });
36
+ }
37
+
38
+ watchStart(options) {
39
+ this.info('watch:start', options);
40
+ }
41
+
42
+ watchEnd() {
43
+ this.info('watch:end');
44
+ }
45
+ }
46
+
47
+ export const buildLogger = new BuildLogger();
@@ -0,0 +1,60 @@
1
+ import { createWriteStream, mkdirSync, existsSync } from 'fs';
2
+ import { dirname, resolve } from 'path';
3
+
4
+ class LogManager {
5
+ constructor(logFile = '.pqm/pqm.log') {
6
+ this.logPath = resolve(process.cwd(), logFile);
7
+ this.stream = null;
8
+ this.initialized = false;
9
+ }
10
+
11
+ init() {
12
+ if (this.initialized) return;
13
+
14
+ try {
15
+ const dir = dirname(this.logPath);
16
+ if (!existsSync(dir)) {
17
+ mkdirSync(dir, { recursive: true });
18
+ }
19
+
20
+ this.stream = createWriteStream(this.logPath, { flags: 'a' });
21
+ this.initialized = true;
22
+ } catch (err) {
23
+ console.warn('Failed to initialize log file:', err.message);
24
+ }
25
+ }
26
+
27
+ write(level, message, meta = {}) {
28
+ if (!this.initialized) this.init();
29
+
30
+ const timestamp = new Date().toISOString();
31
+ const entry = {
32
+ timestamp,
33
+ level,
34
+ message,
35
+ ...meta
36
+ };
37
+
38
+ const line = JSON.stringify(entry) + '\n';
39
+
40
+ if (this.stream) {
41
+ this.stream.write(line);
42
+ }
43
+ }
44
+
45
+ info(message, meta) { this.write('info', message, meta); }
46
+ warn(message, meta) { this.write('warn', message, meta); }
47
+ error(message, meta) { this.write('error', message, meta); }
48
+ debug(message, meta) { this.write('debug', message, meta); }
49
+
50
+ close() {
51
+ if (this.stream) {
52
+ this.stream.end();
53
+ this.stream = null;
54
+ }
55
+ this.initialized = false;
56
+ }
57
+ }
58
+
59
+ export const logManager = new LogManager();
60
+ export default LogManager;
@@ -0,0 +1,282 @@
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * Severity levels
5
+ */
6
+ export const Severity = {
7
+ CRITICAL: 'critical',
8
+ HIGH: 'high',
9
+ MEDIUM: 'medium',
10
+ LOW: 'low',
11
+ INFO: 'info'
12
+ };
13
+
14
+ /**
15
+ * Get severity color
16
+ * @param {string} severity - Severity level
17
+ * @returns {Function} Chalk function
18
+ */
19
+ function getSeverityColor(severity) {
20
+ switch (severity) {
21
+ case Severity.CRITICAL:
22
+ return chalk.red.bold;
23
+ case Severity.HIGH:
24
+ return chalk.red;
25
+ case Severity.MEDIUM:
26
+ return chalk.yellow;
27
+ case Severity.LOW:
28
+ return chalk.blue;
29
+ case Severity.INFO:
30
+ default:
31
+ return chalk.gray;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Get severity icon
37
+ * @param {string} severity - Severity level
38
+ * @returns {string} Icon
39
+ */
40
+ function getSeverityIcon(severity) {
41
+ switch (severity) {
42
+ case Severity.CRITICAL:
43
+ return '🔴';
44
+ case Severity.HIGH:
45
+ return '🟠';
46
+ case Severity.MEDIUM:
47
+ return '🟡';
48
+ case Severity.LOW:
49
+ return '🔵';
50
+ case Severity.INFO:
51
+ default:
52
+ return '⚪';
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Format issue for console output
58
+ * @param {Object} issue - Issue object
59
+ * @returns {string} Formatted string
60
+ */
61
+ function formatIssue(issue) {
62
+ const { severity, type, line, message, suggestion } = issue;
63
+ const color = getSeverityColor(severity);
64
+ const icon = getSeverityIcon(severity);
65
+
66
+ let output = ` ${icon} ${color(`[${severity.toUpperCase()}]`)} ${chalk.cyan(type)}`;
67
+
68
+ if (line) {
69
+ output += ` ${chalk.gray(`(line ${line})`)}`;
70
+ }
71
+
72
+ output += `\n ${message}`;
73
+
74
+ if (suggestion) {
75
+ output += `\n ${chalk.gray('建议:')} ${chalk.green(suggestion)}`;
76
+ }
77
+
78
+ return output;
79
+ }
80
+
81
+ /**
82
+ * Format report for console output
83
+ * @param {Object} report - Analysis report
84
+ * @returns {string} Formatted output
85
+ */
86
+ export function formatConsole(report) {
87
+ const lines = [];
88
+ const { summary, issues, byFile } = report;
89
+
90
+ // Header
91
+ lines.push('');
92
+ lines.push(chalk.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
93
+ lines.push(chalk.bold(' 代码安全分析报告'));
94
+ lines.push(chalk.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
95
+ lines.push('');
96
+
97
+ // Summary
98
+ lines.push(chalk.bold('📊 问题统计:'));
99
+ lines.push(` 🔴 严重: ${summary.critical || 0}`);
100
+ lines.push(` 🟠 高危: ${summary.high || 0}`);
101
+ lines.push(` 🟡 中危: ${summary.medium || 0}`);
102
+ lines.push(` 🔵 低危: ${summary.low || 0}`);
103
+ lines.push(` ⚪ 信息: ${summary.info || 0}`);
104
+ lines.push(` ${chalk.gray('─'.repeat(30))}`);
105
+ lines.push(` 📝 总计: ${chalk.bold(issues.length)} 个问题`);
106
+ lines.push('');
107
+
108
+ if (issues.length === 0) {
109
+ lines.push(chalk.green(' ✓ 没有发现安全问题'));
110
+ lines.push('');
111
+ return lines.join('\n');
112
+ }
113
+
114
+ // Issues by file
115
+ lines.push(chalk.bold('📋 问题详情:'));
116
+ lines.push('');
117
+
118
+ for (const [file, fileIssues] of Object.entries(byFile)) {
119
+ lines.push(chalk.underline(chalk.white(file)));
120
+ lines.push('');
121
+
122
+ for (const issue of fileIssues) {
123
+ lines.push(formatIssue(issue));
124
+ lines.push('');
125
+ }
126
+ }
127
+
128
+ // Footer
129
+ lines.push(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
130
+
131
+ return lines.join('\n');
132
+ }
133
+
134
+ /**
135
+ * Format report as JSON
136
+ * @param {Object} report - Analysis report
137
+ * @returns {string} JSON string
138
+ */
139
+ export function formatJSON(report) {
140
+ return JSON.stringify(report, null, 2);
141
+ }
142
+
143
+ /**
144
+ * Format report as HTML
145
+ * @param {Object} report - Analysis report
146
+ * @returns {string} HTML string
147
+ */
148
+ export function formatHTML(report) {
149
+ const { summary, issues, byFile } = report;
150
+
151
+ const html = `<!DOCTYPE html>
152
+ <html lang="zh-CN">
153
+ <head>
154
+ <meta charset="UTF-8">
155
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
156
+ <title>代码安全分析报告</title>
157
+ <style>
158
+ body {
159
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
160
+ max-width: 1000px;
161
+ margin: 0 auto;
162
+ padding: 20px;
163
+ background: #f5f5f5;
164
+ }
165
+ h1 { color: #333; border-bottom: 2px solid #333; padding-bottom: 10px; }
166
+ .summary {
167
+ background: white;
168
+ padding: 20px;
169
+ border-radius: 8px;
170
+ margin: 20px 0;
171
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
172
+ }
173
+ .summary-item { display: flex; align-items: center; margin: 10px 0; }
174
+ .summary-count { font-size: 24px; font-weight: bold; margin-right: 10px; }
175
+ .critical { color: #dc3545; }
176
+ .high { color: #fd7e14; }
177
+ .medium { color: #ffc107; }
178
+ .low { color: #17a2b8; }
179
+ .info { color: #6c757d; }
180
+ .file-section { margin: 20px 0; }
181
+ .file-path {
182
+ background: #333;
183
+ color: white;
184
+ padding: 10px 15px;
185
+ border-radius: 4px 4px 0 0;
186
+ font-family: monospace;
187
+ }
188
+ .issue {
189
+ background: white;
190
+ padding: 15px;
191
+ margin: 1px 0;
192
+ border-left: 4px solid #ddd;
193
+ }
194
+ .issue.critical { border-left-color: #dc3545; }
195
+ .issue.high { border-left-color: #fd7e14; }
196
+ .issue.medium { border-left-color: #ffc107; }
197
+ .issue.low { border-left-color: #17a2b8; }
198
+ .issue.info { border-left-color: #6c757d; }
199
+ .issue-header { font-weight: bold; margin-bottom: 5px; }
200
+ .issue-message { color: #555; margin: 5px 0; }
201
+ .issue-suggestion { color: #28a745; font-size: 0.9em; }
202
+ .severity-badge {
203
+ display: inline-block;
204
+ padding: 2px 8px;
205
+ border-radius: 4px;
206
+ font-size: 12px;
207
+ color: white;
208
+ margin-right: 10px;
209
+ }
210
+ .severity-badge.critical { background: #dc3545; }
211
+ .severity-badge.high { background: #fd7e14; }
212
+ .severity-badge.medium { background: #ffc107; color: #333; }
213
+ .severity-badge.low { background: #17a2b8; }
214
+ .severity-badge.info { background: #6c757d; }
215
+ </style>
216
+ </head>
217
+ <body>
218
+ <h1>🔒 代码安全分析报告</h1>
219
+
220
+ <div class="summary">
221
+ <h2>📊 问题统计</h2>
222
+ <div class="summary-item"><span class="summary-count critical">${summary.critical || 0}</span> <span>严重</span></div>
223
+ <div class="summary-item"><span class="summary-count high">${summary.high || 0}</span> <span>高危</span></div>
224
+ <div class="summary-item"><span class="summary-count medium">${summary.medium || 0}</span> <span>中危</span></div>
225
+ <div class="summary-item"><span class="summary-count low">${summary.low || 0}</span> <span>低危</span></div>
226
+ <div class="summary-item"><span class="summary-count info">${summary.info || 0}</span> <span>信息</span></div>
227
+ <hr>
228
+ <div class="summary-item"><span class="summary-count">${issues.length}</span> <span>总计问题数</span></div>
229
+ </div>
230
+
231
+ <h2>📋 问题详情</h2>
232
+ ${Object.entries(byFile).map(([file, fileIssues]) => `
233
+ <div class="file-section">
234
+ <div class="file-path">${file}</div>
235
+ ${fileIssues.map(issue => `
236
+ <div class="issue ${issue.severity}">
237
+ <div class="issue-header">
238
+ <span class="severity-badge ${issue.severity}">${issue.severity.toUpperCase()}</span>
239
+ ${issue.type} ${issue.line ? `<span style="color:#666">(line ${issue.line})</span>` : ''}
240
+ </div>
241
+ <div class="issue-message">${issue.message}</div>
242
+ ${issue.suggestion ? `<div class="issue-suggestion">建议: ${issue.suggestion}</div>` : ''}
243
+ </div>
244
+ `).join('')}
245
+ </div>
246
+ `).join('')}
247
+
248
+ <hr>
249
+ <p style="color:#999;text-align:center;">
250
+ Generated by pqm-cli at ${new Date().toISOString()}
251
+ </p>
252
+ </body>
253
+ </html>`;
254
+
255
+ return html;
256
+ }
257
+
258
+ /**
259
+ * Format report based on output type
260
+ * @param {Object} report - Analysis report
261
+ * @param {string} format - Output format (console/json/html)
262
+ * @returns {string} Formatted output
263
+ */
264
+ export function formatReport(report, format = 'console') {
265
+ switch (format) {
266
+ case 'json':
267
+ return formatJSON(report);
268
+ case 'html':
269
+ return formatHTML(report);
270
+ case 'console':
271
+ default:
272
+ return formatConsole(report);
273
+ }
274
+ }
275
+
276
+ export default {
277
+ Severity,
278
+ formatConsole,
279
+ formatJSON,
280
+ formatHTML,
281
+ formatReport
282
+ };