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,61 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
const CONFIG_FILE = '.pqmrc';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
|
+
root: './src',
|
|
9
|
+
exclude: ['node_modules', 'dist', '.git', '**/*.test.js', '**/*.spec.js'],
|
|
10
|
+
buildTool: 'auto',
|
|
11
|
+
buildMode: 'incremental',
|
|
12
|
+
buildOnStart: true,
|
|
13
|
+
webhook: {
|
|
14
|
+
enabled: false,
|
|
15
|
+
port: 3200
|
|
16
|
+
},
|
|
17
|
+
log: {
|
|
18
|
+
level: 'info',
|
|
19
|
+
file: '.pqm/pqm.log'
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default function (program) {
|
|
24
|
+
program
|
|
25
|
+
.command('init')
|
|
26
|
+
.description('Initialize pqm configuration in current directory')
|
|
27
|
+
.option('-f, --force', 'Overwrite existing configuration')
|
|
28
|
+
.action(async (options) => {
|
|
29
|
+
const configPath = path.resolve(process.cwd(), CONFIG_FILE);
|
|
30
|
+
|
|
31
|
+
// Check if config already exists
|
|
32
|
+
if (fs.existsSync(configPath) && !options.force) {
|
|
33
|
+
logger.warn('.pqmrc already exists. Use --force to overwrite.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create logs directory
|
|
38
|
+
const logsDir = path.resolve(process.cwd(), '.pqm');
|
|
39
|
+
if (!fs.existsSync(logsDir)) {
|
|
40
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
41
|
+
logger.info('Created .pqm directory');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Write default configuration
|
|
45
|
+
fs.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
46
|
+
logger.success(`Created ${CONFIG_FILE} with default configuration`);
|
|
47
|
+
|
|
48
|
+
// Create .gitignore entry if .gitignore exists
|
|
49
|
+
const gitignorePath = path.resolve(process.cwd(), '.gitignore');
|
|
50
|
+
if (fs.existsSync(gitignorePath)) {
|
|
51
|
+
const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
|
|
52
|
+
if (!gitignore.includes('.pqm/')) {
|
|
53
|
+
fs.appendFileSync(gitignorePath, '\n# PQM CLI\n.pqm/\n');
|
|
54
|
+
logger.info('Added .pqm/ to .gitignore');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('\nConfiguration created successfully!');
|
|
59
|
+
console.log('Run `pqm watch` to start monitoring.');
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export default function (program) {
|
|
6
|
+
program
|
|
7
|
+
.command('login')
|
|
8
|
+
.description('Login to npm registry')
|
|
9
|
+
.option('--registry <registry>', 'NPM registry URL', 'https://registry.npmjs.org/')
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
console.log(chalk.bold(chalk.cyan('\n🔐 NPM 登录\n')));
|
|
12
|
+
|
|
13
|
+
logger.info(`Registry: ${options.registry}`);
|
|
14
|
+
console.log('');
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// 使用 npm login
|
|
18
|
+
execSync('npm login', {
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
env: { ...process.env }
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// 验证登录
|
|
24
|
+
console.log(chalk.yellow('\n验证登录状态...'));
|
|
25
|
+
const user = execSync('npm whoami', {
|
|
26
|
+
encoding: 'utf8'
|
|
27
|
+
}).trim();
|
|
28
|
+
|
|
29
|
+
if (user) {
|
|
30
|
+
console.log(chalk.green(`\n✅ 登录成功! 用户: ${user}`));
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger.error(`登录失败: ${error.message}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { execSync, spawn } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
// 认证状态
|
|
9
|
+
const AuthStatus = {
|
|
10
|
+
LOGGED_IN: 'logged_in',
|
|
11
|
+
NOT_LOGGED_IN: 'not_logged_in',
|
|
12
|
+
TOKEN_EXPIRED: 'token_expired'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function question(rl, query) {
|
|
16
|
+
return new Promise((resolve) => rl.question(query, resolve));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function execSilent(command) {
|
|
20
|
+
try {
|
|
21
|
+
return execSync(command, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getAuthStatus() {
|
|
28
|
+
const result = {
|
|
29
|
+
status: AuthStatus.NOT_LOGGED_IN,
|
|
30
|
+
user: null,
|
|
31
|
+
registry: 'https://registry.npmjs.org/'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const registry = execSilent('npm config get registry');
|
|
36
|
+
if (registry && registry !== 'undefined') {
|
|
37
|
+
result.registry = registry;
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const user = execSilent('npm whoami');
|
|
43
|
+
if (user && user !== 'undefined') {
|
|
44
|
+
result.status = AuthStatus.LOGGED_IN;
|
|
45
|
+
result.user = user;
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function interactiveLogin(rl) {
|
|
53
|
+
console.log(chalk.cyan('\n启动交互式登录...\n'));
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const child = spawn('npm', ['login'], {
|
|
57
|
+
stdio: 'inherit',
|
|
58
|
+
shell: true
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
child.on('close', () => {
|
|
62
|
+
console.log(chalk.yellow('\n验证登录状态...'));
|
|
63
|
+
const authStatus = getAuthStatus();
|
|
64
|
+
|
|
65
|
+
if (authStatus.status === AuthStatus.LOGGED_IN) {
|
|
66
|
+
console.log(chalk.green(`✓ 登录成功! 用户: ${authStatus.user}`));
|
|
67
|
+
resolve(true);
|
|
68
|
+
} else {
|
|
69
|
+
console.log(chalk.red('❌ 登录验证失败'));
|
|
70
|
+
resolve(false);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function setupTokenAuth(rl) {
|
|
77
|
+
console.log(chalk.cyan('\n🔑 Token 认证'));
|
|
78
|
+
console.log(chalk.gray('获取 Token: https://www.npmjs.com/settings/tokens/new'));
|
|
79
|
+
|
|
80
|
+
const token = await question(rl, chalk.yellow('\n请输入 NPM Token: '));
|
|
81
|
+
|
|
82
|
+
if (!token || token.trim().length < 10) {
|
|
83
|
+
console.log(chalk.red('❌ Token 格式不正确'));
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const registry = execSilent('npm config get registry') || 'https://registry.npmjs.org/';
|
|
89
|
+
const registryKey = registry.replace(/^https?:/, '').replace(/\/$/, '');
|
|
90
|
+
|
|
91
|
+
execSync(`npm config set ${registryKey}/:_authToken=${token.trim()}`, { stdio: 'pipe' });
|
|
92
|
+
|
|
93
|
+
// 验证
|
|
94
|
+
const user = execSilent('npm whoami');
|
|
95
|
+
if (user) {
|
|
96
|
+
console.log(chalk.green(`✓ 认证成功! 用户: ${user}`));
|
|
97
|
+
|
|
98
|
+
// 询问是否保存到项目
|
|
99
|
+
const saveToProject = await question(rl, chalk.yellow('\n保存到项目 .npmrc? (y/N): '));
|
|
100
|
+
if (saveToProject.toLowerCase() === 'y') {
|
|
101
|
+
const npmrcPath = join(process.cwd(), '.npmrc');
|
|
102
|
+
const content = `${registryKey}/:_authToken=${token.trim()}\nregistry=${registry}\n`;
|
|
103
|
+
writeFileSync(npmrcPath, content);
|
|
104
|
+
console.log(chalk.green('✓ Token 已保存到项目'));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.log(chalk.red(`❌ Token 验证失败: ${error.message}`));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function ensureAuth(rl) {
|
|
117
|
+
console.log(chalk.blue('\n🔍 检查 NPM 认证状态...'));
|
|
118
|
+
|
|
119
|
+
const authStatus = getAuthStatus();
|
|
120
|
+
|
|
121
|
+
console.log(chalk.cyan('\n📋 认证状态'));
|
|
122
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
123
|
+
console.log(`Registry: ${authStatus.registry}`);
|
|
124
|
+
|
|
125
|
+
if (authStatus.status === AuthStatus.LOGGED_IN) {
|
|
126
|
+
console.log(`状态: ${chalk.green('✓ 已认证')}`);
|
|
127
|
+
console.log(`用户: ${chalk.green(authStatus.user)}`);
|
|
128
|
+
return authStatus;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`状态: ${chalk.red('✗ 未认证')}`);
|
|
132
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
133
|
+
|
|
134
|
+
console.log(chalk.yellow('\n🔐 请选择认证方式:'));
|
|
135
|
+
console.log(' 1) Token 认证 (推荐)');
|
|
136
|
+
console.log(' 2) 交互式登录 (npm login)');
|
|
137
|
+
console.log(' 0) 退出');
|
|
138
|
+
|
|
139
|
+
const choice = await question(rl, chalk.cyan('\n请选择 (0-2): '));
|
|
140
|
+
|
|
141
|
+
switch (choice.trim()) {
|
|
142
|
+
case '1':
|
|
143
|
+
return await setupTokenAuth(rl) ? getAuthStatus() : null;
|
|
144
|
+
case '2':
|
|
145
|
+
return await interactiveLogin(rl) ? getAuthStatus() : null;
|
|
146
|
+
default:
|
|
147
|
+
console.log(chalk.yellow('已退出'));
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export default function (program) {
|
|
153
|
+
program
|
|
154
|
+
.command('publish')
|
|
155
|
+
.description('Publish package to npm with smart authentication')
|
|
156
|
+
.option('--skip-test', 'Skip running tests')
|
|
157
|
+
.option('--skip-build', 'Skip build step')
|
|
158
|
+
.option('--access <access>', 'Access level (public|restricted)', 'public')
|
|
159
|
+
.action(async (options) => {
|
|
160
|
+
const rl = createInterface({
|
|
161
|
+
input: process.stdin,
|
|
162
|
+
output: process.stdout
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
console.log(chalk.bold(chalk.blue('\n📦 NPM 发布工具\n')));
|
|
167
|
+
|
|
168
|
+
// 1. 检查认证
|
|
169
|
+
const authStatus = await ensureAuth(rl);
|
|
170
|
+
if (!authStatus || authStatus.status !== AuthStatus.LOGGED_IN) {
|
|
171
|
+
console.log(chalk.red('\n❌ 认证失败,无法继续'));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 2. 读取 package.json
|
|
176
|
+
const pkgPath = join(process.cwd(), 'package.json');
|
|
177
|
+
if (!existsSync(pkgPath)) {
|
|
178
|
+
console.log(chalk.red('❌ 未找到 package.json'));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
183
|
+
console.log(chalk.cyan('\n📦 包信息'));
|
|
184
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
185
|
+
console.log(`名称: ${chalk.bold(pkg.name)}`);
|
|
186
|
+
console.log(`版本: ${pkg.version}`);
|
|
187
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
188
|
+
|
|
189
|
+
// 3. 测试
|
|
190
|
+
if (!options.skipTest && pkg.scripts?.test) {
|
|
191
|
+
console.log(chalk.yellow('\n📋 执行测试...'));
|
|
192
|
+
try {
|
|
193
|
+
execSync('npm test', { stdio: 'inherit' });
|
|
194
|
+
console.log(chalk.green('✓ 测试通过'));
|
|
195
|
+
} catch {
|
|
196
|
+
console.log(chalk.red('❌ 测试失败'));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 4. 构建
|
|
202
|
+
if (!options.skipBuild && pkg.scripts?.build) {
|
|
203
|
+
console.log(chalk.yellow('\n🔨 执行构建...'));
|
|
204
|
+
try {
|
|
205
|
+
execSync('npm run build', { stdio: 'inherit' });
|
|
206
|
+
console.log(chalk.green('✓ 构建成功'));
|
|
207
|
+
} catch {
|
|
208
|
+
console.log(chalk.red('❌ 构建失败'));
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 5. 确认发布
|
|
214
|
+
console.log(chalk.cyan('\n🎯 发布确认'));
|
|
215
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
216
|
+
console.log(`包名: ${chalk.bold(pkg.name)}`);
|
|
217
|
+
console.log(`版本: ${chalk.bold(pkg.version)}`);
|
|
218
|
+
console.log(`Registry: ${authStatus.registry}`);
|
|
219
|
+
console.log(`Access: ${options.access}`);
|
|
220
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
221
|
+
|
|
222
|
+
const confirm = await question(rl, chalk.yellow('\n确认发布? (Y/n): '));
|
|
223
|
+
if (confirm.toLowerCase() === 'n') {
|
|
224
|
+
console.log(chalk.yellow('已取消'));
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 6. 发布
|
|
229
|
+
console.log(chalk.yellow('\n🚀 发布到 npm...'));
|
|
230
|
+
try {
|
|
231
|
+
execSync(`npm publish --access ${options.access}`, { stdio: 'inherit' });
|
|
232
|
+
console.log(chalk.green(`\n✅ 发布成功! ${pkg.name}@${pkg.version}`));
|
|
233
|
+
console.log(chalk.cyan(` https://www.npmjs.com/package/${pkg.name}`));
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.log(chalk.red(`\n❌ 发布失败: ${error.message}`));
|
|
236
|
+
|
|
237
|
+
// 检查是否需要 2FA
|
|
238
|
+
const errorOutput = error.message;
|
|
239
|
+
if (errorOutput.includes('OTP') || errorOutput.includes('two-factor')) {
|
|
240
|
+
console.log(chalk.yellow('\n需要双因素认证,请使用 Automation Token'));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
} finally {
|
|
247
|
+
rl.close();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
export default function (program) {
|
|
8
|
+
program
|
|
9
|
+
.command('release [type]')
|
|
10
|
+
.description('Create a new release (patch|minor|major)')
|
|
11
|
+
.option('-p, --pre-release', 'Create pre-release version')
|
|
12
|
+
.option('--skip-push', 'Skip pushing to remote')
|
|
13
|
+
.option('--skip-tag', 'Skip creating git tag')
|
|
14
|
+
.action((type = 'patch', options) => {
|
|
15
|
+
// 验证版本类型
|
|
16
|
+
const validTypes = ['patch', 'minor', 'major'];
|
|
17
|
+
if (!validTypes.includes(type)) {
|
|
18
|
+
logger.error(`版本类型必须是: ${validTypes.join(' | ')}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 检查是否有 package.json
|
|
23
|
+
const pkgPath = join(process.cwd(), 'package.json');
|
|
24
|
+
if (!existsSync(pkgPath)) {
|
|
25
|
+
logger.error('当前目录没有 package.json');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 检查是否有未提交变更
|
|
30
|
+
try {
|
|
31
|
+
const status = execSync('git status --porcelain', {
|
|
32
|
+
encoding: 'utf8'
|
|
33
|
+
}).trim();
|
|
34
|
+
|
|
35
|
+
if (status) {
|
|
36
|
+
logger.error('有未提交的变更,请先提交:');
|
|
37
|
+
console.log(status);
|
|
38
|
+
console.log('\n运行: pqm commit "提交信息"');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// 可能不是 git 仓库,继续
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 读取当前版本
|
|
46
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
47
|
+
logger.info(`当前版本: v${pkg.version}`);
|
|
48
|
+
|
|
49
|
+
// 计算新版本
|
|
50
|
+
const [major, minor, patch] = pkg.version.split('.').map(Number);
|
|
51
|
+
let newVersion;
|
|
52
|
+
|
|
53
|
+
if (type === 'major') newVersion = `${major + 1}.0.0`;
|
|
54
|
+
else if (type === 'minor') newVersion = `${major}.${minor + 1}.0`;
|
|
55
|
+
else newVersion = `${major}.${minor}.${patch + 1}`;
|
|
56
|
+
|
|
57
|
+
logger.info(`新版本: v${newVersion}`);
|
|
58
|
+
|
|
59
|
+
// 确认发布
|
|
60
|
+
console.log(chalk.yellow('\n即将执行以下操作:'));
|
|
61
|
+
console.log(` 1. 更新 package.json 版本到 ${newVersion}`);
|
|
62
|
+
console.log(` 2. 提交变更`);
|
|
63
|
+
if (!options.skipTag) {
|
|
64
|
+
console.log(` 3. 创建标签 v${newVersion}`);
|
|
65
|
+
}
|
|
66
|
+
if (!options.skipPush) {
|
|
67
|
+
console.log(` 4. 推送到远程`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 执行发布
|
|
71
|
+
try {
|
|
72
|
+
// 更新 package.json
|
|
73
|
+
pkg.version = newVersion;
|
|
74
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
75
|
+
logger.success(`版本已更新到 ${newVersion}`);
|
|
76
|
+
|
|
77
|
+
// Git 操作
|
|
78
|
+
if (!options.skipTag || !options.skipPush) {
|
|
79
|
+
const branch = execSync('git branch --show-current', {
|
|
80
|
+
encoding: 'utf8'
|
|
81
|
+
}).trim();
|
|
82
|
+
|
|
83
|
+
execSync('git add package.json', { stdio: 'inherit' });
|
|
84
|
+
execSync(`git commit -m "chore(release): v${newVersion}"`, { stdio: 'inherit' });
|
|
85
|
+
|
|
86
|
+
if (!options.skipTag) {
|
|
87
|
+
execSync(`git tag -a v${newVersion} -m "Release v${newVersion}"`, { stdio: 'inherit' });
|
|
88
|
+
logger.success(`标签 v${newVersion} 已创建`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!options.skipPush) {
|
|
92
|
+
execSync(`git push origin ${branch}`, { stdio: 'inherit' });
|
|
93
|
+
if (!options.skipTag) {
|
|
94
|
+
execSync(`git push origin v${newVersion}`, { stdio: 'inherit' });
|
|
95
|
+
}
|
|
96
|
+
logger.success('已推送到远程');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(chalk.green(`\n🎉 发布成功!v${newVersion}`));
|
|
101
|
+
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.error(`发布失败: ${error.message}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { Spinner } from '../utils/spinner.js';
|
|
6
|
+
import {
|
|
7
|
+
loadGlobalConfig,
|
|
8
|
+
getAIConfig,
|
|
9
|
+
isAIConfigured
|
|
10
|
+
} from '../config/global.js';
|
|
11
|
+
import { createProvider, createAnalyzer, AnalysisType } from '../ai/index.js';
|
|
12
|
+
import { collectFiles } from '../ai/analyzer/collector.js';
|
|
13
|
+
import { formatReport } from '../report/formatter.js';
|
|
14
|
+
|
|
15
|
+
export default function (program) {
|
|
16
|
+
const scanCmd = program
|
|
17
|
+
.command('scan')
|
|
18
|
+
.description('代码安全分析');
|
|
19
|
+
|
|
20
|
+
scanCmd
|
|
21
|
+
.option('--type <type>', '分析类型 (security/quality/dependency)', 'full')
|
|
22
|
+
.option('--fix', '生成修复建议')
|
|
23
|
+
.option('--output <format>', '输出格式 (console/json/html)', 'console')
|
|
24
|
+
.option('--ai', '使用 AI 深度分析')
|
|
25
|
+
.option('--path <path>', '分析路径', '.')
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
await runScan(options);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run code scan
|
|
33
|
+
* @param {Object} options - Scan options
|
|
34
|
+
*/
|
|
35
|
+
async function runScan(options) {
|
|
36
|
+
const {
|
|
37
|
+
type = 'full',
|
|
38
|
+
fix = false,
|
|
39
|
+
output = 'console',
|
|
40
|
+
ai = false,
|
|
41
|
+
path: scanPath = '.'
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
44
|
+
// Resolve path
|
|
45
|
+
const projectPath = path.resolve(process.cwd(), scanPath);
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(projectPath)) {
|
|
48
|
+
logger.error(`路径不存在: ${projectPath}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log(chalk.bold('🔍 代码安全分析'));
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(`目标路径: ${chalk.cyan(projectPath)}`);
|
|
56
|
+
console.log(`分析类型: ${chalk.cyan(type)}`);
|
|
57
|
+
console.log(`输出格式: ${chalk.cyan(output)}`);
|
|
58
|
+
console.log('');
|
|
59
|
+
|
|
60
|
+
// Validate analysis type
|
|
61
|
+
const validTypes = ['security', 'quality', 'dependency', 'full'];
|
|
62
|
+
if (!validTypes.includes(type)) {
|
|
63
|
+
logger.error(`无效的分析类型: ${type}`);
|
|
64
|
+
console.log(`有效类型: ${validTypes.join(', ')}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Validate output format
|
|
69
|
+
const validOutputs = ['console', 'json', 'html'];
|
|
70
|
+
if (!validOutputs.includes(output)) {
|
|
71
|
+
logger.error(`无效的输出格式: ${output}`);
|
|
72
|
+
console.log(`有效格式: ${validOutputs.join(', ')}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Create analyzer options
|
|
77
|
+
const analyzerOptions = {
|
|
78
|
+
projectPath,
|
|
79
|
+
useAI: ai && isAIConfigured()
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Create AI provider if needed
|
|
83
|
+
if (ai) {
|
|
84
|
+
if (!isAIConfigured()) {
|
|
85
|
+
logger.warn('未配置 AI Provider,跳过 AI 分析');
|
|
86
|
+
logger.info('运行 pqm ai config 进行配置');
|
|
87
|
+
console.log('');
|
|
88
|
+
} else {
|
|
89
|
+
const aiConfig = getAIConfig();
|
|
90
|
+
try {
|
|
91
|
+
analyzerOptions.provider = createProvider(aiConfig.provider, {
|
|
92
|
+
apiKey: aiConfig.apiKey,
|
|
93
|
+
model: aiConfig.model
|
|
94
|
+
});
|
|
95
|
+
console.log(chalk.green(`✓ 已启用 AI 分析 (${aiConfig.provider})`));
|
|
96
|
+
console.log('');
|
|
97
|
+
} catch (error) {
|
|
98
|
+
logger.warn(`无法创建 AI Provider: ${error.message}`);
|
|
99
|
+
console.log('');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Create analyzer
|
|
105
|
+
const analyzer = createAnalyzer(analyzerOptions);
|
|
106
|
+
|
|
107
|
+
// Collect files
|
|
108
|
+
const spinner = new Spinner('正在收集项目文件...').start();
|
|
109
|
+
let files;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
files = await collectFiles(projectPath);
|
|
113
|
+
spinner.succeed(`已收集 ${files.length} 个文件`);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
spinner.fail(`文件收集失败: ${error.message}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (files.length === 0) {
|
|
120
|
+
logger.warn('未找到可分析的代码文件');
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(chalk.gray('提示: 确保目标目录包含代码文件'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Run analysis
|
|
127
|
+
spinner.start('正在分析代码...');
|
|
128
|
+
|
|
129
|
+
let report;
|
|
130
|
+
try {
|
|
131
|
+
report = await analyzer.analyzeFiles(files, type);
|
|
132
|
+
spinner.succeed('分析完成');
|
|
133
|
+
} catch (error) {
|
|
134
|
+
spinner.fail(`分析失败: ${error.message}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Add dependency analysis if needed
|
|
139
|
+
if (type === 'dependency' || type === 'full') {
|
|
140
|
+
let depSpinner = new Spinner('正在分析依赖...').start();
|
|
141
|
+
try {
|
|
142
|
+
const depIssues = await analyzer.analyzeDependency(projectPath);
|
|
143
|
+
if (depIssues.length > 0) {
|
|
144
|
+
report.byFile['package.json'] = depIssues;
|
|
145
|
+
report.issues.push(...depIssues);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Recalculate summary
|
|
149
|
+
for (const issue of depIssues) {
|
|
150
|
+
const sev = issue.severity?.toLowerCase();
|
|
151
|
+
if (sev && sev in report.summary) {
|
|
152
|
+
report.summary[sev]++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
depSpinner.succeed(`依赖分析完成 (${depIssues.length} 个问题)`);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
depSpinner.warn(`依赖分析跳过: ${error.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Generate fix suggestions if requested
|
|
163
|
+
if (fix && report.issues.length > 0) {
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(chalk.cyan('生成修复建议...'));
|
|
166
|
+
|
|
167
|
+
for (const issue of report.issues.slice(0, 10)) {
|
|
168
|
+
if (issue.suggestion) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Add auto-generated fix suggestions
|
|
172
|
+
issue.suggestion = generateFixSuggestion(issue);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Output report
|
|
177
|
+
console.log('');
|
|
178
|
+
const formatted = formatReport(report, output);
|
|
179
|
+
|
|
180
|
+
if (output === 'console') {
|
|
181
|
+
console.log(formatted);
|
|
182
|
+
} else if (output === 'json') {
|
|
183
|
+
const outputPath = path.join(projectPath, 'scan-report.json');
|
|
184
|
+
fs.writeFileSync(outputPath, formatted);
|
|
185
|
+
logger.success(`报告已保存到: ${outputPath}`);
|
|
186
|
+
} else if (output === 'html') {
|
|
187
|
+
const outputPath = path.join(projectPath, 'scan-report.html');
|
|
188
|
+
fs.writeFileSync(outputPath, formatted);
|
|
189
|
+
logger.success(`报告已保存到: ${outputPath}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Exit code based on severity
|
|
193
|
+
const hasCritical = report.summary.critical > 0;
|
|
194
|
+
const hasHigh = report.summary.high > 0;
|
|
195
|
+
|
|
196
|
+
if (hasCritical || hasHigh) {
|
|
197
|
+
console.log('');
|
|
198
|
+
if (hasCritical) {
|
|
199
|
+
logger.error(`发现 ${report.summary.critical} 个严重漏洞,请立即修复`);
|
|
200
|
+
}
|
|
201
|
+
if (hasHigh) {
|
|
202
|
+
logger.warn(`发现 ${report.summary.high} 个高危问题`);
|
|
203
|
+
}
|
|
204
|
+
console.log('');
|
|
205
|
+
process.exitCode = 1;
|
|
206
|
+
} else if (report.issues.length > 0) {
|
|
207
|
+
console.log('');
|
|
208
|
+
logger.info(`发现 ${report.issues.length} 个问题,建议处理`);
|
|
209
|
+
console.log('');
|
|
210
|
+
} else {
|
|
211
|
+
console.log('');
|
|
212
|
+
logger.success('没有发现问题');
|
|
213
|
+
console.log('');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Generate fix suggestion based on issue type
|
|
219
|
+
* @param {Object} issue - Issue object
|
|
220
|
+
* @returns {string} Fix suggestion
|
|
221
|
+
*/
|
|
222
|
+
function generateFixSuggestion(issue) {
|
|
223
|
+
const suggestions = {
|
|
224
|
+
'XSS 跨站脚本': '使用 textContent 替代 innerHTML,或使用 DOMPurify 库进行净化',
|
|
225
|
+
'SQL 注入': '使用参数化查询或 ORM 框架',
|
|
226
|
+
'命令注入': '使用 child_process.spawn 并分离命令和参数',
|
|
227
|
+
'危险 eval': '使用 JSON.parse 解析数据,或使用 new Function 配合严格模式',
|
|
228
|
+
'硬编码密钥': '使用环境变量存储敏感信息: process.env.API_KEY',
|
|
229
|
+
'路径遍历': '使用 path.resolve 并验证路径前缀',
|
|
230
|
+
'行过长': '将长行拆分为多行,提高可读性',
|
|
231
|
+
'var 关键字': '使用 const 声明不可变变量,使用 let 声明可变变量',
|
|
232
|
+
'空 catch 块': '添加日志记录或错误转发处理',
|
|
233
|
+
'调试代码': '移除调试代码或使用条件编译'
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return suggestions[issue.type] || '查看相关文档了解修复方案';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export { runScan };
|