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,317 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
import { Spinner } from '../utils/spinner.js';
|
|
4
|
+
import {
|
|
5
|
+
loadGlobalConfig,
|
|
6
|
+
saveGlobalConfig,
|
|
7
|
+
getAIConfig,
|
|
8
|
+
updateAIConfig,
|
|
9
|
+
isAIConfigured,
|
|
10
|
+
CONFIG_FILE
|
|
11
|
+
} from '../config/global.js';
|
|
12
|
+
import { createProvider, getSupportedProviders } from '../ai/index.js';
|
|
13
|
+
import { promptUser, select, promptPassword } from '../utils/prompt.js';
|
|
14
|
+
|
|
15
|
+
export default function (program) {
|
|
16
|
+
const aiCmd = program
|
|
17
|
+
.command('ai')
|
|
18
|
+
.description('AI Provider 配置管理');
|
|
19
|
+
|
|
20
|
+
// ai config - Interactive configuration
|
|
21
|
+
aiCmd
|
|
22
|
+
.command('config')
|
|
23
|
+
.description('交互式配置 AI Provider')
|
|
24
|
+
.option('--list', '查看当前配置')
|
|
25
|
+
.action(async (options) => {
|
|
26
|
+
if (options.list) {
|
|
27
|
+
await showCurrentConfig();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await interactiveConfig();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ai status - Check connection status
|
|
35
|
+
aiCmd
|
|
36
|
+
.command('status')
|
|
37
|
+
.description('检查 AI Provider 连接状态')
|
|
38
|
+
.action(async () => {
|
|
39
|
+
await checkStatus();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ai providers - List supported providers
|
|
43
|
+
aiCmd
|
|
44
|
+
.command('providers')
|
|
45
|
+
.description('列出支持的 AI Provider')
|
|
46
|
+
.action(async () => {
|
|
47
|
+
await listProviders();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ai set - Quick set config
|
|
51
|
+
aiCmd
|
|
52
|
+
.command('set <key> <value>')
|
|
53
|
+
.description('设置配置值')
|
|
54
|
+
.action(async (key, value) => {
|
|
55
|
+
await setConfigValue(key, value);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Show current AI configuration
|
|
61
|
+
*/
|
|
62
|
+
async function showCurrentConfig() {
|
|
63
|
+
const config = loadGlobalConfig();
|
|
64
|
+
const aiConfig = config.ai || {};
|
|
65
|
+
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(chalk.bold('当前 AI 配置:'));
|
|
68
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
69
|
+
console.log(` Provider: ${aiConfig.provider || '(未设置)'}`);
|
|
70
|
+
console.log(` Model: ${aiConfig.model || '(未设置)'}`);
|
|
71
|
+
console.log(` API Key: ${aiConfig.apiKey ? maskAPIKey(aiConfig.apiKey) : '(未设置)'}`);
|
|
72
|
+
console.log(` Enabled: ${aiConfig.enabled !== false ? '是' : '否'}`);
|
|
73
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
74
|
+
console.log(` 配置文件: ${CONFIG_FILE}`);
|
|
75
|
+
console.log('');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Interactive configuration
|
|
80
|
+
*/
|
|
81
|
+
async function interactiveConfig() {
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(chalk.bold('🤖 AI Provider 配置向导'));
|
|
84
|
+
console.log('');
|
|
85
|
+
|
|
86
|
+
const providers = getSupportedProviders();
|
|
87
|
+
|
|
88
|
+
// Step 1: Select provider
|
|
89
|
+
console.log(chalk.cyan('步骤 1: 选择 AI Provider'));
|
|
90
|
+
console.log('');
|
|
91
|
+
|
|
92
|
+
providers.forEach((p, index) => {
|
|
93
|
+
const freeText = p.free ? chalk.green(' (免费额度)') : '';
|
|
94
|
+
console.log(` ${index + 1}. ${chalk.white(p.displayName)}${freeText}`);
|
|
95
|
+
console.log(` ${chalk.gray(p.description)}`);
|
|
96
|
+
console.log(` ${chalk.gray('支持模型: ' + p.models.join(', '))}`);
|
|
97
|
+
console.log('');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const providerAnswer = await promptUser('请输入序号', '1');
|
|
101
|
+
const providerIndex = parseInt(providerAnswer, 10) - 1;
|
|
102
|
+
|
|
103
|
+
if (isNaN(providerIndex) || providerIndex < 0 || providerIndex >= providers.length) {
|
|
104
|
+
logger.error('无效的选择');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const selectedProvider = providers[providerIndex];
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log(chalk.green(`已选择: ${selectedProvider.displayName}`));
|
|
111
|
+
console.log('');
|
|
112
|
+
|
|
113
|
+
// Step 2: Enter API Key
|
|
114
|
+
console.log(chalk.cyan('步骤 2: 输入 API Key'));
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log(chalk.gray(`获取 API Key: ${selectedProvider.website}`));
|
|
117
|
+
console.log('');
|
|
118
|
+
|
|
119
|
+
const apiKey = await promptPassword(`请输入 ${selectedProvider.displayName} API Key`);
|
|
120
|
+
|
|
121
|
+
if (!apiKey || apiKey.trim() === '') {
|
|
122
|
+
logger.error('API Key 不能为空');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Step 3: Select model
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log(chalk.cyan('步骤 3: 选择模型'));
|
|
129
|
+
console.log('');
|
|
130
|
+
|
|
131
|
+
selectedProvider.models.forEach((model, index) => {
|
|
132
|
+
const isDefault = model === selectedProvider.defaultModel;
|
|
133
|
+
const marker = isDefault ? chalk.green(' (推荐)') : '';
|
|
134
|
+
console.log(` ${index + 1}. ${model}${marker}`);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
console.log('');
|
|
138
|
+
const modelAnswer = await promptUser('请输入序号', '1');
|
|
139
|
+
const modelIndex = parseInt(modelAnswer, 10) - 1;
|
|
140
|
+
|
|
141
|
+
let selectedModel;
|
|
142
|
+
if (isNaN(modelIndex) || modelIndex < 0 || modelIndex >= selectedProvider.models.length) {
|
|
143
|
+
selectedModel = selectedProvider.defaultModel;
|
|
144
|
+
} else {
|
|
145
|
+
selectedModel = selectedProvider.models[modelIndex];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log(chalk.green(`已选择模型: ${selectedModel}`));
|
|
150
|
+
console.log('');
|
|
151
|
+
|
|
152
|
+
// Step 4: Confirm
|
|
153
|
+
console.log(chalk.cyan('步骤 4: 确认配置'));
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
156
|
+
console.log(` Provider: ${selectedProvider.displayName}`);
|
|
157
|
+
console.log(` Model: ${selectedModel}`);
|
|
158
|
+
console.log(` API Key: ${maskAPIKey(apiKey)}`);
|
|
159
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
160
|
+
console.log('');
|
|
161
|
+
|
|
162
|
+
const confirm = await promptUser('确认保存配置? (y/N)', 'y');
|
|
163
|
+
|
|
164
|
+
if (confirm.toLowerCase() !== 'y') {
|
|
165
|
+
logger.info('配置已取消');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Save configuration
|
|
170
|
+
updateAIConfig({
|
|
171
|
+
provider: selectedProvider.name,
|
|
172
|
+
apiKey: apiKey.trim(),
|
|
173
|
+
model: selectedModel,
|
|
174
|
+
enabled: true
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Try to validate
|
|
178
|
+
console.log('');
|
|
179
|
+
const spinner = new Spinner('正在验证 API 连接...').start();
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const provider = createProvider(selectedProvider.name, {
|
|
183
|
+
apiKey: apiKey.trim(),
|
|
184
|
+
model: selectedModel
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const isValid = await provider.validateConfig();
|
|
188
|
+
|
|
189
|
+
if (isValid) {
|
|
190
|
+
spinner.succeed(chalk.green('API 连接验证成功'));
|
|
191
|
+
} else {
|
|
192
|
+
spinner.warn(chalk.yellow('API 连接验证失败,请检查 API Key'));
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
spinner.warn(chalk.yellow(`验证出错: ${error.message}`));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log('');
|
|
199
|
+
logger.success(`配置已保存到: ${CONFIG_FILE}`);
|
|
200
|
+
console.log('');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check AI connection status
|
|
205
|
+
*/
|
|
206
|
+
async function checkStatus() {
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(chalk.bold('🔍 AI Provider 状态检查'));
|
|
209
|
+
console.log('');
|
|
210
|
+
|
|
211
|
+
const aiConfig = getAIConfig();
|
|
212
|
+
|
|
213
|
+
if (!aiConfig || !aiConfig.apiKey) {
|
|
214
|
+
logger.warn('尚未配置 AI Provider');
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(chalk.gray('运行 pqm ai config 进行配置'));
|
|
217
|
+
console.log('');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log(`Provider: ${aiConfig.provider}`);
|
|
222
|
+
console.log(`Model: ${aiConfig.model}`);
|
|
223
|
+
console.log('');
|
|
224
|
+
|
|
225
|
+
const spinner = new Spinner('正在连接...').start();
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const provider = createProvider(aiConfig.provider, {
|
|
229
|
+
apiKey: aiConfig.apiKey,
|
|
230
|
+
model: aiConfig.model
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const isValid = await provider.validateConfig();
|
|
234
|
+
|
|
235
|
+
if (isValid) {
|
|
236
|
+
spinner.succeed(chalk.green('连接成功'));
|
|
237
|
+
} else {
|
|
238
|
+
spinner.fail(chalk.red('连接失败'));
|
|
239
|
+
console.log('');
|
|
240
|
+
console.log(chalk.yellow('可能的原因:'));
|
|
241
|
+
console.log(' - API Key 无效或已过期');
|
|
242
|
+
console.log(' - 网络连接问题');
|
|
243
|
+
console.log(' - API 服务不可用');
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
spinner.fail(chalk.red('连接失败'));
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log(chalk.red(`错误: ${error.message}`));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log('');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* List supported providers
|
|
256
|
+
*/
|
|
257
|
+
async function listProviders() {
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log(chalk.bold('📋 支持的 AI Provider'));
|
|
260
|
+
console.log('');
|
|
261
|
+
|
|
262
|
+
const providers = getSupportedProviders();
|
|
263
|
+
|
|
264
|
+
providers.forEach(p => {
|
|
265
|
+
const freeText = p.free ? chalk.green(' ✓ 免费') : '';
|
|
266
|
+
console.log(` ${chalk.cyan(p.displayName)}${freeText}`);
|
|
267
|
+
console.log(` ${chalk.gray(p.description)}`);
|
|
268
|
+
console.log(` 默认模型: ${p.defaultModel}`);
|
|
269
|
+
console.log(` 支持模型: ${p.models.join(', ')}`);
|
|
270
|
+
console.log(` 获取 API Key: ${p.website}`);
|
|
271
|
+
console.log('');
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Set configuration value
|
|
277
|
+
*/
|
|
278
|
+
async function setConfigValue(key, value) {
|
|
279
|
+
const validKeys = ['provider', 'model', 'apiKey', 'enabled'];
|
|
280
|
+
|
|
281
|
+
if (!validKeys.includes(key)) {
|
|
282
|
+
logger.error(`无效的配置项: ${key}`);
|
|
283
|
+
console.log('');
|
|
284
|
+
console.log(`有效的配置项: ${validKeys.join(', ')}`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const config = loadGlobalConfig();
|
|
289
|
+
|
|
290
|
+
if (!config.ai) {
|
|
291
|
+
config.ai = {};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Handle boolean values
|
|
295
|
+
if (value === 'true') {
|
|
296
|
+
config.ai[key] = true;
|
|
297
|
+
} else if (value === 'false') {
|
|
298
|
+
config.ai[key] = false;
|
|
299
|
+
} else {
|
|
300
|
+
config.ai[key] = value;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
saveGlobalConfig(config);
|
|
304
|
+
logger.success(`已设置 ${key} = ${key === 'apiKey' ? maskAPIKey(value) : value}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Mask API key for display
|
|
309
|
+
* @param {string} key - API key
|
|
310
|
+
* @returns {string} Masked key
|
|
311
|
+
*/
|
|
312
|
+
function maskAPIKey(key) {
|
|
313
|
+
if (!key || key.length < 8) {
|
|
314
|
+
return '****';
|
|
315
|
+
}
|
|
316
|
+
return key.substring(0, 4) + '****' + key.substring(key.length - 4);
|
|
317
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { logger } from '../utils/logger.js';
|
|
2
|
+
|
|
3
|
+
export default function (program) {
|
|
4
|
+
program
|
|
5
|
+
.command('build')
|
|
6
|
+
.description('Trigger a manual build')
|
|
7
|
+
.option('-t, --tool <tool>', 'Build tool (vite/rollup)', 'vite')
|
|
8
|
+
.option('-m, --mode <mode>', 'Build mode (development/production)', 'production')
|
|
9
|
+
.action(async (options) => {
|
|
10
|
+
logger.info(`Starting build with ${options.tool}...`);
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const { buildProject } = await import('../core/builder.js');
|
|
14
|
+
await buildProject({
|
|
15
|
+
tool: options.tool,
|
|
16
|
+
mode: options.mode
|
|
17
|
+
});
|
|
18
|
+
logger.success('Build completed successfully');
|
|
19
|
+
} catch (err) {
|
|
20
|
+
logger.error(`Build failed: ${err.message}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
export default function (program) {
|
|
5
|
+
program
|
|
6
|
+
.command('commit [message]')
|
|
7
|
+
.description('Commit and push changes to git')
|
|
8
|
+
.option('-m, --message <message>', 'Commit message')
|
|
9
|
+
.action((message, options) => {
|
|
10
|
+
const commitMessage = message || options.message;
|
|
11
|
+
|
|
12
|
+
if (!commitMessage) {
|
|
13
|
+
logger.error('请提供提交信息');
|
|
14
|
+
console.log('用法: pqm commit "提交信息"');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 检查是否有远程仓库
|
|
19
|
+
const remoteUrl = execSync('git remote get-url origin 2>/dev/null', {
|
|
20
|
+
encoding: 'utf8'
|
|
21
|
+
}).trim();
|
|
22
|
+
|
|
23
|
+
if (!remoteUrl) {
|
|
24
|
+
logger.error('未配置远程仓库');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 获取当前分支
|
|
29
|
+
const branch = execSync('git branch --show-current', {
|
|
30
|
+
encoding: 'utf8'
|
|
31
|
+
}).trim();
|
|
32
|
+
|
|
33
|
+
logger.info(`远程: ${remoteUrl}`);
|
|
34
|
+
logger.info(`分支: ${branch}`);
|
|
35
|
+
|
|
36
|
+
// 检查是否有变更
|
|
37
|
+
const status = execSync('git status --porcelain', {
|
|
38
|
+
encoding: 'utf8'
|
|
39
|
+
}).trim();
|
|
40
|
+
|
|
41
|
+
if (!status) {
|
|
42
|
+
logger.warn('没有变更需要提交');
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('\n变更文件:');
|
|
47
|
+
status.split('\n').forEach(line => {
|
|
48
|
+
console.log(` ${line}`);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 执行提交
|
|
52
|
+
try {
|
|
53
|
+
logger.info('执行: git add .');
|
|
54
|
+
execSync('git add .', { stdio: 'inherit' });
|
|
55
|
+
|
|
56
|
+
logger.info(`执行: git commit -m "${commitMessage}"`);
|
|
57
|
+
execSync(`git commit -m "${commitMessage}"`, { stdio: 'inherit' });
|
|
58
|
+
|
|
59
|
+
logger.info(`执行: git push origin ${branch}`);
|
|
60
|
+
execSync(`git push origin ${branch}`, { stdio: 'inherit' });
|
|
61
|
+
|
|
62
|
+
logger.success('提交并推送成功!');
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logger.error(`操作失败: ${error.message}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
export default function (program) {
|
|
8
|
+
const configCmd = program
|
|
9
|
+
.command('config')
|
|
10
|
+
.description('View or manage configuration');
|
|
11
|
+
|
|
12
|
+
configCmd
|
|
13
|
+
.command('list')
|
|
14
|
+
.description('List current configuration')
|
|
15
|
+
.action(() => {
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
console.log('\nCurrent Configuration:');
|
|
18
|
+
console.log(JSON.stringify(config, null, 2));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
configCmd
|
|
22
|
+
.command('get <key>')
|
|
23
|
+
.description('Get a configuration value')
|
|
24
|
+
.action((key) => {
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
const value = getNestedValue(config, key);
|
|
27
|
+
if (value === undefined) {
|
|
28
|
+
logger.warn(`Key '${key}' not found`);
|
|
29
|
+
} else if (typeof value === 'object') {
|
|
30
|
+
console.log(JSON.stringify(value, null, 2));
|
|
31
|
+
} else {
|
|
32
|
+
console.log(value);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
configCmd
|
|
37
|
+
.command('set <key> <value>')
|
|
38
|
+
.description('Set a configuration value')
|
|
39
|
+
.action((key, value) => {
|
|
40
|
+
const configPath = path.resolve(process.cwd(), CONFIG_FILE);
|
|
41
|
+
let config = {};
|
|
42
|
+
|
|
43
|
+
if (fs.existsSync(configPath)) {
|
|
44
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setNestedValue(config, key, parseValue(value));
|
|
48
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
49
|
+
logger.success(`Set ${key} = ${value}`);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function loadConfig() {
|
|
54
|
+
const configPath = path.resolve(process.cwd(), CONFIG_FILE);
|
|
55
|
+
const defaultConfig = {
|
|
56
|
+
root: './src',
|
|
57
|
+
exclude: ['node_modules', 'dist', '.git', '**/*.test.js', '**/*.spec.js'],
|
|
58
|
+
buildTool: 'auto',
|
|
59
|
+
buildMode: 'incremental',
|
|
60
|
+
buildOnStart: true,
|
|
61
|
+
webhook: {
|
|
62
|
+
enabled: false,
|
|
63
|
+
port: 3200
|
|
64
|
+
},
|
|
65
|
+
log: {
|
|
66
|
+
level: 'info',
|
|
67
|
+
file: '.pqm/pqm.log'
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (fs.existsSync(configPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
74
|
+
return { ...defaultConfig, ...userConfig };
|
|
75
|
+
} catch (err) {
|
|
76
|
+
logger.warn('Failed to parse .pqmrc, using defaults');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return defaultConfig;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getNestedValue(obj, key) {
|
|
84
|
+
const keys = key.split('.');
|
|
85
|
+
let result = obj;
|
|
86
|
+
for (const k of keys) {
|
|
87
|
+
result = result?.[k];
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function setNestedValue(obj, key, value) {
|
|
93
|
+
const keys = key.split('.');
|
|
94
|
+
let current = obj;
|
|
95
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
96
|
+
if (!current[keys[i]]) {
|
|
97
|
+
current[keys[i]] = {};
|
|
98
|
+
}
|
|
99
|
+
current = current[keys[i]];
|
|
100
|
+
}
|
|
101
|
+
current[keys[keys.length - 1]] = value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseValue(value) {
|
|
105
|
+
// Try to parse as JSON for booleans, numbers, arrays, objects
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(value);
|
|
108
|
+
} catch {
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { loadConfig };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
export default function (program) {
|
|
8
|
+
program
|
|
9
|
+
.command('doctor')
|
|
10
|
+
.description('Diagnose the environment and configuration')
|
|
11
|
+
.action(() => {
|
|
12
|
+
console.log('\n' + chalk.bold('PQM Environment Diagnostics\n'));
|
|
13
|
+
|
|
14
|
+
const checks = [
|
|
15
|
+
checkNodeVersion,
|
|
16
|
+
checkNpmVersion,
|
|
17
|
+
checkConfigFile,
|
|
18
|
+
checkSrcDirectory,
|
|
19
|
+
checkBuildTools,
|
|
20
|
+
checkDependencies
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
let passed = 0;
|
|
24
|
+
let failed = 0;
|
|
25
|
+
|
|
26
|
+
for (const check of checks) {
|
|
27
|
+
try {
|
|
28
|
+
const result = check();
|
|
29
|
+
if (result.passed) {
|
|
30
|
+
logger.success(result.message);
|
|
31
|
+
passed++;
|
|
32
|
+
} else {
|
|
33
|
+
logger.error(result.message);
|
|
34
|
+
if (result.hint) {
|
|
35
|
+
console.log(chalk.gray(` → ${result.hint}`));
|
|
36
|
+
}
|
|
37
|
+
failed++;
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
logger.error(`Check failed: ${err.message}`);
|
|
41
|
+
failed++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('\n' + chalk.bold('Summary:'));
|
|
46
|
+
console.log(` ${chalk.green('Passed')}: ${passed}`);
|
|
47
|
+
console.log(` ${chalk.red('Failed')}: ${failed}`);
|
|
48
|
+
|
|
49
|
+
if (failed === 0) {
|
|
50
|
+
console.log(`\n${chalk.green("All checks passed! You're ready to use pqm.")}`);
|
|
51
|
+
} else {
|
|
52
|
+
console.log(`\n${chalk.yellow('Some checks failed. Please fix the issues above.')}`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function checkNodeVersion() {
|
|
58
|
+
const version = process.version;
|
|
59
|
+
const major = parseInt(version.slice(1).split('.')[0], 10);
|
|
60
|
+
return {
|
|
61
|
+
passed: major >= 18,
|
|
62
|
+
message: `Node.js version: ${version}`,
|
|
63
|
+
hint: major < 18 ? 'Upgrade to Node.js 18 or later' : null
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function checkNpmVersion() {
|
|
68
|
+
try {
|
|
69
|
+
const version = execSync('npm --version', { encoding: 'utf-8' }).trim();
|
|
70
|
+
return { passed: true, message: `npm version: ${version}` };
|
|
71
|
+
} catch {
|
|
72
|
+
return { passed: false, message: 'npm not found' };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkConfigFile() {
|
|
77
|
+
const configPath = resolve(process.cwd(), '.pqmrc');
|
|
78
|
+
const exists = existsSync(configPath);
|
|
79
|
+
return {
|
|
80
|
+
passed: exists,
|
|
81
|
+
message: exists ? '.pqmrc found' : '.pqmrc not found',
|
|
82
|
+
hint: exists ? null : 'Run `pqm init` to create configuration'
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function checkSrcDirectory() {
|
|
87
|
+
const srcPath = resolve(process.cwd(), 'src');
|
|
88
|
+
const exists = existsSync(srcPath);
|
|
89
|
+
return {
|
|
90
|
+
passed: exists,
|
|
91
|
+
message: exists ? 'src/ directory found' : 'src/ directory not found',
|
|
92
|
+
hint: exists ? null : 'Create a src/ directory or update .pqmrc root path'
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function checkBuildTools() {
|
|
97
|
+
const cwd = process.cwd();
|
|
98
|
+
const tools = [];
|
|
99
|
+
|
|
100
|
+
if (existsSync(join(cwd, 'vite.config.js')) ||
|
|
101
|
+
existsSync(join(cwd, 'vite.config.ts'))) {
|
|
102
|
+
tools.push('vite');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (existsSync(join(cwd, 'rollup.config.js')) ||
|
|
106
|
+
existsSync(join(cwd, 'rollup.config.mjs'))) {
|
|
107
|
+
tools.push('rollup');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (existsSync(join(cwd, 'tsconfig.json'))) {
|
|
111
|
+
tools.push('typescript');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
passed: tools.length > 0,
|
|
116
|
+
message: tools.length > 0
|
|
117
|
+
? `Build tools detected: ${tools.join(', ')}`
|
|
118
|
+
: 'No build tools detected',
|
|
119
|
+
hint: tools.length === 0 ? 'Install vite, rollup, or typescript' : null
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function checkDependencies() {
|
|
124
|
+
const pkgPath = join(process.cwd(), 'package.json');
|
|
125
|
+
|
|
126
|
+
if (!existsSync(pkgPath)) {
|
|
127
|
+
return {
|
|
128
|
+
passed: false,
|
|
129
|
+
message: 'package.json not found',
|
|
130
|
+
hint: 'Run `npm init` to create package.json'
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
136
|
+
const hasVite = pkg.devDependencies?.vite || pkg.dependencies?.vite;
|
|
137
|
+
const hasRollup = pkg.devDependencies?.rollup || pkg.dependencies?.rollup;
|
|
138
|
+
|
|
139
|
+
if (hasVite || hasRollup) {
|
|
140
|
+
return { passed: true, message: 'Build dependencies found' };
|
|
141
|
+
}
|
|
142
|
+
return { passed: true, message: 'package.json found (no build deps)' };
|
|
143
|
+
} catch {
|
|
144
|
+
return { passed: false, message: 'Failed to parse package.json' };
|
|
145
|
+
}
|
|
146
|
+
}
|