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.
- package/README.md +254 -0
- package/bin/pqm.js +6 -0
- package/package.json +31 -0
- package/src/ai/analyzer/collector.js +191 -0
- package/src/ai/analyzer/dependency.js +269 -0
- package/src/ai/analyzer/index.js +234 -0
- package/src/ai/analyzer/quality.js +241 -0
- package/src/ai/analyzer/security.js +302 -0
- package/src/ai/index.js +16 -0
- package/src/ai/providers/bailian.js +121 -0
- package/src/ai/providers/base.js +177 -0
- package/src/ai/providers/deepseek.js +100 -0
- package/src/ai/providers/index.js +100 -0
- package/src/ai/providers/openai.js +100 -0
- package/src/builders/base.js +35 -0
- package/src/builders/rollup.js +47 -0
- package/src/builders/vite.js +47 -0
- package/src/cli.js +41 -0
- package/src/commands/ai.js +317 -0
- package/src/commands/build.js +24 -0
- package/src/commands/commit.js +68 -0
- package/src/commands/config.js +113 -0
- package/src/commands/doctor.js +146 -0
- package/src/commands/init.js +61 -0
- package/src/commands/login.js +37 -0
- package/src/commands/publish.js +250 -0
- package/src/commands/release.js +107 -0
- package/src/commands/scan.js +239 -0
- package/src/commands/status.js +129 -0
- package/src/commands/watch.js +170 -0
- package/src/commands/webhook.js +240 -0
- package/src/config/detector.js +82 -0
- package/src/config/global.js +136 -0
- package/src/config/loader.js +49 -0
- package/src/core/builder.js +88 -0
- package/src/index.js +5 -0
- package/src/logs/build.js +47 -0
- package/src/logs/manager.js +60 -0
- package/src/report/formatter.js +282 -0
- package/src/utils/http.js +130 -0
- package/src/utils/logger.js +24 -0
- package/src/utils/prompt.js +132 -0
- 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
|
+
};
|