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,129 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
export default function (program) {
|
|
7
|
+
program
|
|
8
|
+
.command('status')
|
|
9
|
+
.description('Show project status and recent changes')
|
|
10
|
+
.action(() => {
|
|
11
|
+
console.log(chalk.bold(chalk.cyan('\n📊 项目状态\n')));
|
|
12
|
+
|
|
13
|
+
// 读取 package.json
|
|
14
|
+
const pkgPath = join(process.cwd(), 'package.json');
|
|
15
|
+
if (existsSync(pkgPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
18
|
+
console.log(chalk.cyan('📦 包信息'));
|
|
19
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
20
|
+
console.log(`名称: ${chalk.bold(pkg.name || 'N/A')}`);
|
|
21
|
+
console.log(`版本: ${chalk.green('v' + (pkg.version || '0.0.0'))}`);
|
|
22
|
+
if (pkg.description) {
|
|
23
|
+
console.log(`描述: ${pkg.description}`);
|
|
24
|
+
}
|
|
25
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
26
|
+
} catch {
|
|
27
|
+
console.log(chalk.red('无法读取 package.json'));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Git 状态
|
|
32
|
+
try {
|
|
33
|
+
const branch = execSync('git branch --show-current 2>/dev/null', {
|
|
34
|
+
encoding: 'utf8'
|
|
35
|
+
}).trim();
|
|
36
|
+
|
|
37
|
+
if (branch) {
|
|
38
|
+
const remote = execSync('git remote get-url origin 2>/dev/null', {
|
|
39
|
+
encoding: 'utf8'
|
|
40
|
+
}).trim();
|
|
41
|
+
|
|
42
|
+
const status = execSync('git status --porcelain 2>/dev/null', {
|
|
43
|
+
encoding: 'utf8'
|
|
44
|
+
}).trim();
|
|
45
|
+
|
|
46
|
+
console.log(chalk.cyan('\n🌿 Git 状态'));
|
|
47
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
48
|
+
console.log(`分支: ${chalk.green(branch)}`);
|
|
49
|
+
if (remote) {
|
|
50
|
+
console.log(`远程: ${remote}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (status) {
|
|
54
|
+
const lines = status.split('\n').filter(l => l.trim());
|
|
55
|
+
console.log(`变更: ${chalk.yellow(lines.length + ' 个文件')}`);
|
|
56
|
+
lines.slice(0, 5).forEach(line => {
|
|
57
|
+
console.log(` ${line}`);
|
|
58
|
+
});
|
|
59
|
+
if (lines.length > 5) {
|
|
60
|
+
console.log(chalk.gray(` ... 还有 ${lines.length - 5} 个文件`));
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
console.log(`变更: ${chalk.green('工作区干净')}`);
|
|
64
|
+
}
|
|
65
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
66
|
+
|
|
67
|
+
// 最近的提交
|
|
68
|
+
try {
|
|
69
|
+
const recentCommits = execSync('git log --oneline -5 2>/dev/null', {
|
|
70
|
+
encoding: 'utf8'
|
|
71
|
+
}).trim();
|
|
72
|
+
|
|
73
|
+
if (recentCommits) {
|
|
74
|
+
console.log(chalk.cyan('\n📝 最近提交'));
|
|
75
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
76
|
+
recentCommits.split('\n').forEach(line => {
|
|
77
|
+
console.log(` ${line}`);
|
|
78
|
+
});
|
|
79
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// 无提交历史
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 最近的标签
|
|
86
|
+
try {
|
|
87
|
+
const lastTag = execSync('git describe --tags --abbrev=0 2>/dev/null', {
|
|
88
|
+
encoding: 'utf8'
|
|
89
|
+
}).trim();
|
|
90
|
+
|
|
91
|
+
if (lastTag) {
|
|
92
|
+
console.log(chalk.cyan('\n🏷 最新标签'));
|
|
93
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
94
|
+
console.log(` ${chalk.green(lastTag)}`);
|
|
95
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// 没有标签
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// 不是 Git 仓库,忽略
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// NPM 认证状态
|
|
106
|
+
try {
|
|
107
|
+
const user = execSync('npm whoami 2>/dev/null', {
|
|
108
|
+
encoding: 'utf8'
|
|
109
|
+
}).trim();
|
|
110
|
+
|
|
111
|
+
const registry = execSync('npm config get registry 2>/dev/null', {
|
|
112
|
+
encoding: 'utf8'
|
|
113
|
+
}).trim();
|
|
114
|
+
|
|
115
|
+
console.log(chalk.cyan('\n🔐 NPM 状态'));
|
|
116
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
117
|
+
console.log(`用户: ${user && user !== 'undefined' ? chalk.green(user) : chalk.red('未登录')}`);
|
|
118
|
+
console.log(`Registry: ${registry || 'https://registry.npmjs.org/'}`);
|
|
119
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
120
|
+
} catch {
|
|
121
|
+
console.log(chalk.cyan('\n🔐 NPM 状态'));
|
|
122
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
123
|
+
console.log(`用户: ${chalk.red('未登录')}`);
|
|
124
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('');
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
import { buildProject } from '../core/builder.js';
|
|
7
|
+
|
|
8
|
+
// Debounce timer for build triggers
|
|
9
|
+
let buildTimeout = null;
|
|
10
|
+
|
|
11
|
+
export default function (program) {
|
|
12
|
+
program
|
|
13
|
+
.command('watch')
|
|
14
|
+
.description('Start file watcher and auto build')
|
|
15
|
+
.option('-p, --path <path>', 'Watch path', './src')
|
|
16
|
+
.option('-t, --tool <tool>', 'Build tool (vite/rollup)')
|
|
17
|
+
.option('-m, --mode <mode>', 'Build mode (incremental/full)', 'incremental')
|
|
18
|
+
.option('--webhook', 'Enable webhook receiver')
|
|
19
|
+
.option('--port <port>', 'Webhook port', '3200')
|
|
20
|
+
.option('--poll', 'Use polling', false)
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
logger.info('Starting pqm watch...');
|
|
23
|
+
console.log('');
|
|
24
|
+
|
|
25
|
+
// Load config
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
const watchPath = options.path || config.root || './src';
|
|
28
|
+
const buildTool = options.tool || config.buildTool || 'auto';
|
|
29
|
+
const buildMode = options.mode || config.buildMode || 'incremental';
|
|
30
|
+
|
|
31
|
+
logger.info(`Watch path: ${watchPath}`);
|
|
32
|
+
logger.info(`Build tool: ${buildTool}`);
|
|
33
|
+
logger.info(`Build mode: ${buildMode}`);
|
|
34
|
+
logger.info(`Platform: ${process.platform}`);
|
|
35
|
+
|
|
36
|
+
// Use polling on Windows by default
|
|
37
|
+
const usePolling = options.poll || process.platform === 'win32';
|
|
38
|
+
logger.info(`Polling: ${usePolling ? 'enabled' : 'disabled'}`);
|
|
39
|
+
console.log('');
|
|
40
|
+
|
|
41
|
+
// Excludes
|
|
42
|
+
const excludes = config.exclude || ['node_modules', 'dist', '.git'];
|
|
43
|
+
|
|
44
|
+
// Create watcher
|
|
45
|
+
const watcher = chokidar.watch(watchPath, {
|
|
46
|
+
ignored: [
|
|
47
|
+
...excludes,
|
|
48
|
+
'**/node_modules/**',
|
|
49
|
+
'**/dist/**',
|
|
50
|
+
'**/.git/**',
|
|
51
|
+
'**/*.log',
|
|
52
|
+
'**/.pqm/**'
|
|
53
|
+
],
|
|
54
|
+
persistent: true,
|
|
55
|
+
ignoreInitial: true,
|
|
56
|
+
usePolling: usePolling,
|
|
57
|
+
interval: 100,
|
|
58
|
+
binaryInterval: 300,
|
|
59
|
+
awaitWriteFinish: {
|
|
60
|
+
stabilityThreshold: 200,
|
|
61
|
+
pollInterval: 50
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Helper function to get relative path
|
|
66
|
+
const getRelativePath = (filePath) => {
|
|
67
|
+
return filePath.replace(process.cwd(), '.').replace(/\\/g, '/');
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Event handlers
|
|
71
|
+
const handleChange = (event, filePath) => {
|
|
72
|
+
const relPath = getRelativePath(filePath);
|
|
73
|
+
const timestamp = new Date().toISOString().slice(11, 19);
|
|
74
|
+
|
|
75
|
+
const eventIcon = {
|
|
76
|
+
add: chalk.green('+'),
|
|
77
|
+
change: chalk.yellow('~'),
|
|
78
|
+
unlink: chalk.red('-')
|
|
79
|
+
}[event] || '?';
|
|
80
|
+
|
|
81
|
+
console.log(`[${timestamp}] ${eventIcon} ${relPath}`);
|
|
82
|
+
|
|
83
|
+
// Trigger build with debounce
|
|
84
|
+
if (event !== 'unlink') {
|
|
85
|
+
clearTimeout(buildTimeout);
|
|
86
|
+
buildTimeout = setTimeout(() => {
|
|
87
|
+
triggerBuild(buildTool, buildMode);
|
|
88
|
+
}, 300);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Register event handlers
|
|
93
|
+
watcher.on('add', (path) => handleChange('add', path));
|
|
94
|
+
watcher.on('change', (path) => handleChange('change', path));
|
|
95
|
+
watcher.on('unlink', (path) => handleChange('unlink', path));
|
|
96
|
+
|
|
97
|
+
watcher.on('ready', () => {
|
|
98
|
+
logger.success('Watcher ready');
|
|
99
|
+
logger.info(`Watching: ${watchPath}`);
|
|
100
|
+
console.log('');
|
|
101
|
+
logger.info('Press Ctrl+C to exit');
|
|
102
|
+
|
|
103
|
+
// Build on start if configured
|
|
104
|
+
if (config.buildOnStart) {
|
|
105
|
+
logger.build(buildMode);
|
|
106
|
+
triggerBuild(buildTool, buildMode);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
watcher.on('error', (err) => {
|
|
111
|
+
logger.error(`Watcher error: ${err.message}`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Webhook server
|
|
115
|
+
if (options.webhook || config.webhook?.enabled) {
|
|
116
|
+
const port = parseInt(options.port) || config.webhook?.port || 3200;
|
|
117
|
+
startWebhookServer(port);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Cleanup on exit
|
|
121
|
+
const cleanup = () => {
|
|
122
|
+
console.log('');
|
|
123
|
+
logger.info('Shutting down...');
|
|
124
|
+
watcher.close().then(() => {
|
|
125
|
+
logger.success('Watcher closed');
|
|
126
|
+
process.exit(0);
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
process.on('SIGINT', cleanup);
|
|
131
|
+
process.on('SIGTERM', cleanup);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function triggerBuild(tool, mode) {
|
|
136
|
+
buildProject({ tool, mode })
|
|
137
|
+
.then(() => {
|
|
138
|
+
logger.success('Build completed');
|
|
139
|
+
})
|
|
140
|
+
.catch(err => {
|
|
141
|
+
logger.error(`Build failed: ${err.message}`);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function startWebhookServer(port) {
|
|
146
|
+
const server = http.createServer((req, res) => {
|
|
147
|
+
if (req.method === 'POST' && req.url === '/trigger') {
|
|
148
|
+
let body = '';
|
|
149
|
+
req.on('data', chunk => body += chunk);
|
|
150
|
+
req.on('end', () => {
|
|
151
|
+
try {
|
|
152
|
+
const data = JSON.parse(body);
|
|
153
|
+
logger.info(`Webhook received: ${data.action || 'trigger'}`);
|
|
154
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
155
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
156
|
+
} catch {
|
|
157
|
+
res.writeHead(400);
|
|
158
|
+
res.end('Invalid JSON');
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
} else {
|
|
162
|
+
res.writeHead(404);
|
|
163
|
+
res.end('Not found');
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
server.listen(port, () => {
|
|
168
|
+
logger.info(`Webhook server on port ${port}`);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { createHmac } from 'crypto';
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
export default function (program) {
|
|
11
|
+
const webhookCmd = program
|
|
12
|
+
.command('webhook')
|
|
13
|
+
.description('Webhook server for auto npm publish');
|
|
14
|
+
|
|
15
|
+
// 启动 webhook 服务器
|
|
16
|
+
webhookCmd
|
|
17
|
+
.command('start')
|
|
18
|
+
.description('Start webhook server for auto publish')
|
|
19
|
+
.option('-p, --port <port>', 'Server port', '3000')
|
|
20
|
+
.option('-s, --secret <secret>', 'Webhook secret for signature verification')
|
|
21
|
+
.action((options) => {
|
|
22
|
+
const PORT = parseInt(options.port) || 3000;
|
|
23
|
+
const SECRET = options.secret || '';
|
|
24
|
+
const NPM_TOKEN = process.env.NPM_TOKEN;
|
|
25
|
+
|
|
26
|
+
console.log(chalk.bold(chalk.cyan('\n🌐 NPM 发布 Webhook 服务器\n')));
|
|
27
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
28
|
+
|
|
29
|
+
// 检查 NPM_TOKEN
|
|
30
|
+
if (!NPM_TOKEN) {
|
|
31
|
+
logger.warn('未设置 NPM_TOKEN 环境变量');
|
|
32
|
+
console.log(chalk.yellow('建议: export NPM_TOKEN=your_token'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
logger.info(`端口: ${PORT}`);
|
|
36
|
+
logger.info(`签名验证: ${SECRET ? '已启用' : '未启用'}`);
|
|
37
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
38
|
+
|
|
39
|
+
// 验证签名
|
|
40
|
+
const verifySignature = (body, signature) => {
|
|
41
|
+
if (!SECRET) return true;
|
|
42
|
+
const hmac = createHmac('sha256', SECRET);
|
|
43
|
+
hmac.update(body);
|
|
44
|
+
const digest = `sha256=${hmac.digest('hex')}`;
|
|
45
|
+
return digest === signature;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// 处理发布
|
|
49
|
+
const handlePublish = async (tag, repoUrl) => {
|
|
50
|
+
const workDir = join(tmpdir(), `npm-publish-${Date.now()}`);
|
|
51
|
+
mkdirSync(workDir, { recursive: true });
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
console.log(chalk.cyan('\n🚀 开始发布流程...'));
|
|
55
|
+
logger.info(`标签: ${tag}`);
|
|
56
|
+
logger.info(`工作目录: ${workDir}`);
|
|
57
|
+
|
|
58
|
+
// 1. 克隆代码
|
|
59
|
+
console.log(chalk.yellow('\n1️⃣ 克隆代码...'));
|
|
60
|
+
execSync(`git clone --depth 1 --branch ${tag} ${repoUrl} .`, {
|
|
61
|
+
cwd: workDir,
|
|
62
|
+
stdio: 'inherit',
|
|
63
|
+
env: { ...process.env, NPM_TOKEN }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 2. 安装依赖
|
|
67
|
+
console.log(chalk.yellow('\n2️⃣ 安装依赖...'));
|
|
68
|
+
execSync('npm ci', {
|
|
69
|
+
cwd: workDir,
|
|
70
|
+
stdio: 'inherit',
|
|
71
|
+
env: { ...process.env, NPM_TOKEN }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// 3. 运行测试
|
|
75
|
+
console.log(chalk.yellow('\n3️⃣ 运行测试...'));
|
|
76
|
+
try {
|
|
77
|
+
execSync('npm test', {
|
|
78
|
+
cwd: workDir,
|
|
79
|
+
stdio: 'inherit',
|
|
80
|
+
env: { ...process.env, NPM_TOKEN }
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
logger.warn('测试失败或未配置');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. 构建
|
|
87
|
+
console.log(chalk.yellow('\n4️⃣ 构建...'));
|
|
88
|
+
try {
|
|
89
|
+
execSync('npm run build', {
|
|
90
|
+
cwd: workDir,
|
|
91
|
+
stdio: 'inherit',
|
|
92
|
+
env: { ...process.env, NPM_TOKEN }
|
|
93
|
+
});
|
|
94
|
+
} catch {
|
|
95
|
+
logger.warn('构建失败或未配置');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 5. 发布
|
|
99
|
+
console.log(chalk.yellow('\n5️⃣ 发布到 npm...'));
|
|
100
|
+
const npmrcContent = `//registry.npmjs.org/:_authToken=${NPM_TOKEN}\nregistry=https://registry.npmjs.org/\n`;
|
|
101
|
+
writeFileSync(join(workDir, '.npmrc'), npmrcContent);
|
|
102
|
+
|
|
103
|
+
execSync('npm publish --access public', {
|
|
104
|
+
cwd: workDir,
|
|
105
|
+
stdio: 'inherit',
|
|
106
|
+
env: { ...process.env, NPM_TOKEN }
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
console.log(chalk.green('\n✅ 发布成功!'));
|
|
110
|
+
return { success: true };
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.log(chalk.red(`\n❌ 发布失败: ${error.message}`));
|
|
114
|
+
return { success: false, error: error.message };
|
|
115
|
+
} finally {
|
|
116
|
+
console.log(chalk.yellow('\n🧹 清理工作目录...'));
|
|
117
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// 创建服务器
|
|
122
|
+
const server = http.createServer(async (req, res) => {
|
|
123
|
+
if (req.method !== 'POST' || req.url !== '/webhook/npm-publish') {
|
|
124
|
+
res.statusCode = 404;
|
|
125
|
+
res.end('Not Found');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let body = '';
|
|
130
|
+
req.on('data', chunk => body += chunk);
|
|
131
|
+
req.on('end', async () => {
|
|
132
|
+
try {
|
|
133
|
+
// 验证签名
|
|
134
|
+
const signature = req.headers['x-hub-signature-256'] || '';
|
|
135
|
+
if (!verifySignature(body, signature)) {
|
|
136
|
+
res.statusCode = 401;
|
|
137
|
+
res.end(JSON.stringify({ error: 'Invalid signature' }));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 解析数据
|
|
142
|
+
const data = JSON.parse(body);
|
|
143
|
+
const ref = data.ref || '';
|
|
144
|
+
|
|
145
|
+
// 只处理标签推送
|
|
146
|
+
if (!ref.startsWith('refs/tags/')) {
|
|
147
|
+
res.statusCode = 200;
|
|
148
|
+
res.end(JSON.stringify({ message: 'Not a tag push' }));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const tag = ref.replace('refs/tags/', '');
|
|
153
|
+
const repoUrl = data.repository?.git_http_url ||
|
|
154
|
+
data.repository?.url ||
|
|
155
|
+
data.repository?.clone_url;
|
|
156
|
+
|
|
157
|
+
if (!repoUrl) {
|
|
158
|
+
res.statusCode = 400;
|
|
159
|
+
res.end(JSON.stringify({ error: 'Missing repository URL' }));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(chalk.cyan(`\n📥 收到标签推送: ${tag}`));
|
|
164
|
+
logger.info(`仓库: ${repoUrl}`);
|
|
165
|
+
|
|
166
|
+
// 立即响应
|
|
167
|
+
res.statusCode = 202;
|
|
168
|
+
res.end(JSON.stringify({ status: 'publishing', tag, repo: repoUrl }));
|
|
169
|
+
|
|
170
|
+
// 异步处理
|
|
171
|
+
await handlePublish(tag, repoUrl);
|
|
172
|
+
|
|
173
|
+
} catch (error) {
|
|
174
|
+
logger.error(`Webhook error: ${error.message}`);
|
|
175
|
+
res.statusCode = 500;
|
|
176
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
server.listen(PORT, () => {
|
|
182
|
+
console.log(chalk.green('\n✅ Webhook 服务器已启动'));
|
|
183
|
+
console.log(chalk.cyan(` http://localhost:${PORT}/webhook/npm-publish`));
|
|
184
|
+
console.log(chalk.gray('\n按 Ctrl+C 停止'));
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// 测试 webhook
|
|
189
|
+
webhookCmd
|
|
190
|
+
.command('test')
|
|
191
|
+
.description('Test webhook endpoint')
|
|
192
|
+
.option('-p, --port <port>', 'Server port', '3000')
|
|
193
|
+
.option('-t, --tag <tag>', 'Test tag', 'v1.0.0')
|
|
194
|
+
.option('-r, --repo <repo>', 'Test repo URL')
|
|
195
|
+
.action((options) => {
|
|
196
|
+
const PORT = options.port || '3000';
|
|
197
|
+
const tag = options.tag || 'v1.0.0';
|
|
198
|
+
const repo = options.repo || 'https://github.com/test/repo.git';
|
|
199
|
+
|
|
200
|
+
const testData = JSON.stringify({
|
|
201
|
+
ref: `refs/tags/${tag}`,
|
|
202
|
+
repository: {
|
|
203
|
+
git_http_url: repo,
|
|
204
|
+
url: repo,
|
|
205
|
+
clone_url: repo
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
console.log(chalk.cyan('\n🧪 测试 Webhook\n'));
|
|
210
|
+
console.log(`POST http://localhost:${PORT}/webhook/npm-publish`);
|
|
211
|
+
console.log('Body:', testData);
|
|
212
|
+
|
|
213
|
+
const req = http.request({
|
|
214
|
+
hostname: 'localhost',
|
|
215
|
+
port: PORT,
|
|
216
|
+
path: '/webhook/npm-publish',
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: {
|
|
219
|
+
'Content-Type': 'application/json',
|
|
220
|
+
'Content-Length': Buffer.byteLength(testData)
|
|
221
|
+
}
|
|
222
|
+
}, (res) => {
|
|
223
|
+
let data = '';
|
|
224
|
+
res.on('data', chunk => data += chunk);
|
|
225
|
+
res.on('end', () => {
|
|
226
|
+
console.log(chalk.green(`\n响应: ${res.statusCode}`));
|
|
227
|
+
console.log(data);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
req.on('error', (err) => {
|
|
232
|
+
console.log(chalk.red(`\n连接失败: ${err.message}`));
|
|
233
|
+
console.log(chalk.yellow('请确保 webhook 服务器正在运行:'));
|
|
234
|
+
console.log(' pqm webhook start');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
req.write(testData);
|
|
238
|
+
req.end();
|
|
239
|
+
});
|
|
240
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function detectProjectConfig(cwd = process.cwd()) {
|
|
5
|
+
const result = {
|
|
6
|
+
type: 'unknown',
|
|
7
|
+
buildTool: null,
|
|
8
|
+
srcDir: null,
|
|
9
|
+
distDir: null,
|
|
10
|
+
hasPackageJson: false
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Check for package.json
|
|
14
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
15
|
+
if (fs.existsSync(pkgPath)) {
|
|
16
|
+
result.hasPackageJson = true;
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
19
|
+
|
|
20
|
+
// Get source and dist directories from package.json
|
|
21
|
+
if (pkg.main) result.distDir = pkg.main.startsWith('dist') ? 'dist' : null;
|
|
22
|
+
if (pkg.source) result.srcDir = path.dirname(pkg.source);
|
|
23
|
+
if (pkg.exports) {
|
|
24
|
+
// Check for dist in exports
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Detect build tool from dependencies
|
|
28
|
+
if (pkg.devDependencies?.vite || pkg.dependencies?.vite) {
|
|
29
|
+
result.buildTool = 'vite';
|
|
30
|
+
} else if (pkg.devDependencies?.rollup || pkg.dependencies?.rollup) {
|
|
31
|
+
result.buildTool = 'rollup';
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore parse errors
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check for config files
|
|
39
|
+
if (fs.existsSync(path.join(cwd, 'vite.config.js')) ||
|
|
40
|
+
fs.existsSync(path.join(cwd, 'vite.config.ts'))) {
|
|
41
|
+
result.buildTool = 'vite';
|
|
42
|
+
result.type = 'vite';
|
|
43
|
+
} else if (fs.existsSync(path.join(cwd, 'rollup.config.js')) ||
|
|
44
|
+
fs.existsSync(path.join(cwd, 'rollup.config.mjs'))) {
|
|
45
|
+
result.buildTool = 'rollup';
|
|
46
|
+
result.type = 'rollup';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Detect source directory
|
|
50
|
+
if (!result.srcDir) {
|
|
51
|
+
const possibleDirs = ['src', 'source', 'lib', 'packages'];
|
|
52
|
+
for (const dir of possibleDirs) {
|
|
53
|
+
if (fs.existsSync(path.join(cwd, dir))) {
|
|
54
|
+
result.srcDir = dir;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Detect TypeScript
|
|
61
|
+
if (fs.existsSync(path.join(cwd, 'tsconfig.json'))) {
|
|
62
|
+
result.type = result.type === 'unknown' ? 'typescript' : result.type;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function autoDetectConfig() {
|
|
69
|
+
const detected = detectProjectConfig();
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
root: detected.srcDir || './src',
|
|
73
|
+
exclude: ['node_modules', 'dist', '.git', '**/*.test.js', '**/*.spec.js'],
|
|
74
|
+
buildTool: detected.buildTool || 'auto',
|
|
75
|
+
buildMode: 'incremental',
|
|
76
|
+
buildOnStart: true,
|
|
77
|
+
webhook: {
|
|
78
|
+
enabled: false,
|
|
79
|
+
port: 3200
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|