@airiot/cli 1.0.3 → 1.0.5
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/bin/iot-scripts.js +52 -2
- package/config/eslint.config.js +13 -1
- package/package.json +1 -1
- package/scripts/bugfix.js +212 -0
package/bin/iot-scripts.js
CHANGED
|
@@ -52,16 +52,66 @@ switch (script) {
|
|
|
52
52
|
case 'i18n-scanner':
|
|
53
53
|
case 'i18n-translate':
|
|
54
54
|
case 'eslint':
|
|
55
|
+
case 'bugfix':
|
|
55
56
|
case 'test': {
|
|
56
57
|
await runScript(script);
|
|
57
58
|
break;
|
|
58
59
|
}
|
|
59
60
|
case 'deploy': {
|
|
60
61
|
await runScript('build');
|
|
61
|
-
await runScript('
|
|
62
|
+
await runScript('install');
|
|
62
63
|
break;
|
|
63
64
|
}
|
|
65
|
+
case 'help':
|
|
64
66
|
default:
|
|
65
|
-
|
|
67
|
+
// 使用 chalk 打印美观的使用说明
|
|
68
|
+
const chalk = (await import('chalk')).default;
|
|
69
|
+
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(chalk.bold.cyan('Airiot CLI — 使用说明\n'));
|
|
72
|
+
console.log(`${chalk.green('用法:')} ${chalk.white.bold('air <command> [options]')}`);
|
|
73
|
+
console.log(`${chalk.green('说明:')} 在命令名前传入 Node 参数(例如调试参数),在命令名后传入脚本参数。\n`);
|
|
74
|
+
|
|
75
|
+
const pad = (s, w = 12) => s + ' '.repeat(Math.max(0, w - s.length));
|
|
76
|
+
|
|
77
|
+
const commands = [
|
|
78
|
+
['build', '构建项目,生成可部署包'],
|
|
79
|
+
['start', '本地启动开发服务器(开发模式)'],
|
|
80
|
+
['preview', '预览构建后的产物(静态预览)'],
|
|
81
|
+
['test', '运行测试用例'],
|
|
82
|
+
['upload', '(不推荐)使用老的接口将构建产物上传到目标平台/服务器'],
|
|
83
|
+
['install', '将构建产物安装到目标平台/服务器)'],
|
|
84
|
+
['pack', '打包为发布包'],
|
|
85
|
+
['deploy', '先构建再上传(内部等价于 build -> install'],
|
|
86
|
+
['i18n-scanner', '扫描并提取国际化文本'],
|
|
87
|
+
['i18n-translate', '对提取的文本进行翻译处理'],
|
|
88
|
+
['eslint', '运行 ESLint 检查代码'],
|
|
89
|
+
['bugfix', '使用AI修复Bug'],
|
|
90
|
+
['help', '显示此帮助信息']
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
console.log(chalk.yellow('可用命令:'));
|
|
94
|
+
for (const [cmd, desc] of commands) {
|
|
95
|
+
console.log(' ' + chalk.cyan(pad(cmd)) + ' ' + chalk.dim(desc));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log();
|
|
99
|
+
console.log(chalk.yellow('示例:'));
|
|
100
|
+
console.log(' ' + chalk.cyan('air build') + ' ' + chalk.gray('# 生产构建'));
|
|
101
|
+
console.log(' ' + chalk.cyan('air start') + ' ' + chalk.gray('# 本地开发并热重载'));
|
|
102
|
+
console.log(' ' + chalk.cyan('air deploy') + ' ' + chalk.gray('# 构建并上传'));
|
|
103
|
+
console.log();
|
|
104
|
+
console.log(chalk.yellow('Node 参数:'));
|
|
105
|
+
console.log(' ' + chalk.white('如果需要传递 Node 参数(例如 --fix),请将它们放在命令前面:'));
|
|
106
|
+
console.log(' ' + chalk.cyan('air --fix eslint') + ' ' + chalk.gray('# 启用调试'));
|
|
107
|
+
console.log();
|
|
108
|
+
|
|
109
|
+
if (script) {
|
|
110
|
+
console.log(
|
|
111
|
+
chalk.red('未知的脚本') + ' ' +
|
|
112
|
+
chalk.bold(`"${script}"`) + '。' +
|
|
113
|
+
' ' + chalk.gray('运行') + ' ' + chalk.cyan('air help') + ' ' + chalk.gray('以查看可用命令。')
|
|
114
|
+
);
|
|
115
|
+
}
|
|
66
116
|
break;
|
|
67
117
|
}
|
package/config/eslint.config.js
CHANGED
|
@@ -6,6 +6,17 @@ import reactPlugin from 'eslint-plugin-react';
|
|
|
6
6
|
import importPlugin from 'eslint-plugin-import';
|
|
7
7
|
import paths from './paths.js';
|
|
8
8
|
|
|
9
|
+
let appEsLintConfig = [];
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const module = await import(new URL(`file://${paths.appPath}/eslint.config.js`));
|
|
13
|
+
appEsLintConfig = Array.isArray(module.default) ? module.default : [ module.default ];
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (err.code !== 'ERR_MODULE_NOT_FOUND') {
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
export default defineConfig([
|
|
10
21
|
// 基础 JS 配置
|
|
11
22
|
{
|
|
@@ -88,5 +99,6 @@ export default defineConfig([
|
|
|
88
99
|
{
|
|
89
100
|
ignores: ['**/node_modules/**', '**/dist/**'],
|
|
90
101
|
rules: {}
|
|
91
|
-
}
|
|
102
|
+
},
|
|
103
|
+
...appEsLintConfig
|
|
92
104
|
]);
|
package/package.json
CHANGED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { ESLint } from 'eslint';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import paths from "../config/paths.js";
|
|
4
|
+
import overrideConfig from '../config/eslint.config.js';
|
|
5
|
+
|
|
6
|
+
const { exec } = await import('child_process');
|
|
7
|
+
const { promisify } = await import('util');
|
|
8
|
+
const execP = promisify(exec);
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
// 检查 cline 是否可用
|
|
12
|
+
await execP('cline version', { maxBuffer: 1024 * 1024 });
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.error(chalk.red('未检测到 cline CLI,脚本需要 cline 才能运行。'));
|
|
15
|
+
console.log('请按下面任一方式安装并重试:');
|
|
16
|
+
console.log(chalk.cyan(' npm install -g cline'));
|
|
17
|
+
console.log(chalk.cyan(' 或者:yarn global add cline'));
|
|
18
|
+
console.log(chalk.cyan(' 或参考 cline 官方文档获取其它安装方式'));
|
|
19
|
+
console.log(chalk.cyan(' 注意:安装cline后要配置可用的Provider'));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const eslint = new ESLint({
|
|
24
|
+
overrideConfigFile: true,
|
|
25
|
+
overrideConfig,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const fixRules = '1. Remove console.log statements; 2. For duplicate object keys, keep the latter key and remove the earlier one';
|
|
29
|
+
|
|
30
|
+
// 简单的彩色输出封装
|
|
31
|
+
const symbols = {
|
|
32
|
+
info: chalk.cyan('ℹ'),
|
|
33
|
+
step: chalk.blue('▶'),
|
|
34
|
+
success: chalk.green('✓'),
|
|
35
|
+
warn: chalk.yellow('⚠'),
|
|
36
|
+
error: chalk.red('✖'),
|
|
37
|
+
};
|
|
38
|
+
const log = {
|
|
39
|
+
info: (...args) => console.log(symbols.info, ...args),
|
|
40
|
+
step: (...args) => console.log(symbols.step, ...args),
|
|
41
|
+
success: (...args) => console.log(symbols.success, ...args),
|
|
42
|
+
warn: (...args) => console.warn(symbols.warn, ...args),
|
|
43
|
+
error: (...args) => console.error(symbols.error, ...args),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
(async () => {
|
|
47
|
+
log.step(chalk.bold('执行 ESLint...'));
|
|
48
|
+
const results = await eslint.lintFiles(['src/**/*.js']);
|
|
49
|
+
|
|
50
|
+
// 转换为按文件的数组,保留必要字段
|
|
51
|
+
const files = results.map(r => ({
|
|
52
|
+
filePath: r.filePath,
|
|
53
|
+
messages: r.messages.map(m => ({
|
|
54
|
+
ruleId: m.ruleId,
|
|
55
|
+
message: m.message,
|
|
56
|
+
line: m.line,
|
|
57
|
+
column: m.column,
|
|
58
|
+
})),
|
|
59
|
+
raw: r,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
log.success(`ESLint 执行完毕,找到 ${chalk.yellow(files.length)} 个文件有问题。`);
|
|
63
|
+
|
|
64
|
+
// 创建 10 个 instance 并记录 address 到数组
|
|
65
|
+
// 并行创建 10 个 instance
|
|
66
|
+
const createCmd = 'cline i n';
|
|
67
|
+
const maxBuffer = 10 * 1024 * 1024;
|
|
68
|
+
const createPromises = Array.from({ length: 10 }, (_, i) =>
|
|
69
|
+
execP(createCmd, { maxBuffer })
|
|
70
|
+
.then(({ stdout, stderr }) => ({
|
|
71
|
+
index: i + 1,
|
|
72
|
+
cmd: createCmd,
|
|
73
|
+
stdout: (typeof stdout === 'string' ? stdout : String(stdout || '')).trim(),
|
|
74
|
+
stderr: (typeof stderr === 'string' ? stderr : String(stderr || '')).trim(),
|
|
75
|
+
error: false,
|
|
76
|
+
}))
|
|
77
|
+
.catch(err => ({
|
|
78
|
+
index: i + 1,
|
|
79
|
+
cmd: createCmd,
|
|
80
|
+
stdout: err && err.stdout ? String(err.stdout).trim() : '',
|
|
81
|
+
stderr: err && err.stderr ? String(err.stderr).trim() : (err.message || String(err)),
|
|
82
|
+
error: true,
|
|
83
|
+
}))
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
log.step(chalk.bold('创建 cline instances...'));
|
|
87
|
+
const instances = await Promise.all(createPromises);
|
|
88
|
+
log.success('创建 cline instances 完毕');
|
|
89
|
+
|
|
90
|
+
// 从每个实例输出中提取 address
|
|
91
|
+
for (const it of instances) {
|
|
92
|
+
const out = it.stdout || '';
|
|
93
|
+
let address = null;
|
|
94
|
+
const ipv4 = out.match(/((?:\d{1,3}\.){3}\d{1,3})(?::\d+)?/);
|
|
95
|
+
if (ipv4) address = ipv4[0];
|
|
96
|
+
else {
|
|
97
|
+
const labeled = out.match(/Address[:\s]*([^\s,;]+)/i);
|
|
98
|
+
if (labeled) address = labeled[1];
|
|
99
|
+
}
|
|
100
|
+
if (!address) address = out.split(/\r?\n/).find(Boolean) || out || null;
|
|
101
|
+
it.address = address;
|
|
102
|
+
if (!it.error && address) {
|
|
103
|
+
log.info(`实例 ${chalk.bold(it.index)} 地址: ${chalk.green(address)}`);
|
|
104
|
+
} else {
|
|
105
|
+
log.error(`创建实例 ${chalk.bold(it.index)} 失败: ${chalk.red(it.stderr || 'unknown error')}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 使用实例作为并发 worker,从文件队列中轮询领取任务,完成后继续领取,直到队列为空
|
|
109
|
+
const queue = files
|
|
110
|
+
.filter(f => f && f.messages && f.messages.length > 0)
|
|
111
|
+
.map(f => ({ ...f })); // shallow copy,避免后续对原数组影响
|
|
112
|
+
|
|
113
|
+
if (queue.length === 0) {
|
|
114
|
+
log.info(chalk.yellow('没有需要修复的文件,跳过修复流程。'));
|
|
115
|
+
} else {
|
|
116
|
+
log.step(`开始分派 ${chalk.yellow(queue.length)} 个待修复文件给 ${chalk.yellow(instances.length)} 个实例并行处理...`);
|
|
117
|
+
|
|
118
|
+
const results = []; // 存放每个任务的执行结果
|
|
119
|
+
|
|
120
|
+
// worker:每个实例执行一个循环,直到 queue 为空
|
|
121
|
+
const workers = instances.map((it, idx) => (async () => {
|
|
122
|
+
// 跳过无地址或创建失败的实例
|
|
123
|
+
if (!it || it.error || !it.address) {
|
|
124
|
+
log.warn(`跳过实例 ${chalk.bold(idx + 1)},无可用地址或创建失败。`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
while (true) {
|
|
129
|
+
// 取出下一个文件(单线程 JS 中 shift 是安全的)
|
|
130
|
+
const f = queue.shift();
|
|
131
|
+
if (!f) break;
|
|
132
|
+
|
|
133
|
+
const bugContent = f.messages
|
|
134
|
+
.map(m => `${m.message} (rule:${m.ruleId} ${m.line}:${m.column})`)
|
|
135
|
+
.join('; ');
|
|
136
|
+
const payload = `fix bug. the rules: ${fixRules}; file: ${f.filePath}; bug content: ${bugContent}`;
|
|
137
|
+
const cmd = `cline -y --address ${it.address} ${JSON.stringify(payload)}`;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
log.step(`实例 ${chalk.bold(idx + 1)} 正在修复: ${chalk.cyan(f.filePath.replace(paths.appPath, ''))}`);
|
|
141
|
+
const { stdout, stderr } = await execP(cmd, { maxBuffer });
|
|
142
|
+
log.success(`实例 ${chalk.bold(idx + 1)} 完成修复: ${chalk.cyan(f.filePath.replace(paths.appPath, ''))}`);
|
|
143
|
+
results.push({
|
|
144
|
+
filePath: f.filePath,
|
|
145
|
+
cmd,
|
|
146
|
+
stdout: typeof stdout === 'string' ? stdout : String(stdout || ''),
|
|
147
|
+
stderr: typeof stderr === 'string' ? stderr : String(stderr || ''),
|
|
148
|
+
status: 'fulfilled',
|
|
149
|
+
});
|
|
150
|
+
} catch (err) {
|
|
151
|
+
log.error(`实例 ${chalk.bold(idx + 1)} 修复失败: ${chalk.red(f.filePath)}`);
|
|
152
|
+
results.push({
|
|
153
|
+
filePath: f.filePath,
|
|
154
|
+
cmd,
|
|
155
|
+
stdout: err && err.stdout ? String(err.stdout) : '',
|
|
156
|
+
stderr: err && err.stderr ? String(err.stderr) : (err.message || String(err)),
|
|
157
|
+
status: 'rejected',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
})());
|
|
162
|
+
|
|
163
|
+
// 等待所有 worker 完成
|
|
164
|
+
await Promise.all(workers);
|
|
165
|
+
|
|
166
|
+
// 打印结果表格(与后续原代码输出风格相同)
|
|
167
|
+
const rows = results.map(s => {
|
|
168
|
+
const stdout = (s.stdout || '').trim();
|
|
169
|
+
const stderr = (s.stderr || '').trim();
|
|
170
|
+
let result;
|
|
171
|
+
if (s.status === 'fulfilled' && !stderr) result = chalk.green('已修复');
|
|
172
|
+
else if (s.status === 'fulfilled' && stderr) result = chalk.yellow('部分修复/有警告');
|
|
173
|
+
else if (s.status === 'rejected') result = chalk.red('修复失败');
|
|
174
|
+
else result = chalk.gray('未执行');
|
|
175
|
+
const noteRaw = result === chalk.green('已修复')
|
|
176
|
+
? (stdout.split(/\r?\n/)[0] || '无输出')
|
|
177
|
+
: (stderr.split(/\r?\n/)[0] || stdout.split(/\r?\n/)[0] || (s.cmd ? '命令已执行' : '未知错误'));
|
|
178
|
+
return { file: s.filePath.replace(paths.appPath, ''), result: String(result), note: noteRaw };
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (rows.length === 0) {
|
|
182
|
+
log.info(chalk.yellow('没有需要修复的文件。'));
|
|
183
|
+
} else {
|
|
184
|
+
const fileCol = Math.max(4, ...rows.map(r => r.file.length));
|
|
185
|
+
const resCol = Math.max(6, ...rows.map(r => r.result.length));
|
|
186
|
+
const noteCol = Math.max(4, ...rows.map(r => r.note.length));
|
|
187
|
+
|
|
188
|
+
const pad = (str, len) => str + ' '.repeat(len - str.length);
|
|
189
|
+
const sep = '-'.repeat(fileCol) + ' ' + '-'.repeat(resCol) + ' ' + '-'.repeat(noteCol);
|
|
190
|
+
|
|
191
|
+
console.log(chalk.bold(pad('文件', fileCol)) + ' ' + chalk.bold(pad('结果', resCol)) + ' ' + chalk.bold(pad('说明', noteCol)));
|
|
192
|
+
console.log(chalk.gray(sep));
|
|
193
|
+
for (const r of rows) {
|
|
194
|
+
console.log(pad(r.file, fileCol) + ' ' + pad(r.result, resCol) + ' ' + pad(r.note, noteCol));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const total = rows.length;
|
|
198
|
+
const success = rows.filter(r => r.result.includes('已修复')).length;
|
|
199
|
+
const partial = rows.filter(r => r.result.includes('部分修复')).length;
|
|
200
|
+
const failed = rows.filter(r => r.result.includes('修复失败')).length;
|
|
201
|
+
console.log('\n' +
|
|
202
|
+
chalk.bold('汇总:') + ' ' +
|
|
203
|
+
`总计 ${chalk.yellow(total)},` +
|
|
204
|
+
`已修复 ${chalk.green(success)},` +
|
|
205
|
+
`部分/警告 ${chalk.yellow(partial)},` +
|
|
206
|
+
`失败 ${chalk.red(failed)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 任务已经由此处完整处理,终止后续重复执行
|
|
211
|
+
process.exit(0);
|
|
212
|
+
})();
|