@fastcar/cli 0.1.3 → 0.1.4
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/cli.js +239 -235
- package/package.json +1 -1
- package/skills/AGENTS.md +251 -0
- package/skills/fastcar-database/SKILL.md +225 -49
- package/skills/fastcar-framework/SKILL.md +577 -576
- package/src/init.js +708 -700
- package/src/skill.js +493 -364
package/src/skill.js
CHANGED
|
@@ -1,364 +1,493 @@
|
|
|
1
|
-
const inquirer = require('inquirer');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { getTarget, getGlobalPath, getLocalPath, getAllTargets, getTargetNames } = require('./skill-targets');
|
|
5
|
-
|
|
6
|
-
const fsPromises = fs.promises;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 获取包根目录
|
|
10
|
-
*/
|
|
11
|
-
function getPackageRoot() {
|
|
12
|
-
return path.join(__dirname, '..');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 获取 skills 目录
|
|
17
|
-
*/
|
|
18
|
-
function getSkillsDir() {
|
|
19
|
-
return path.join(getPackageRoot(), 'skills');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* 检查路径是否存在
|
|
24
|
-
*/
|
|
25
|
-
async function pathExists(checkPath) {
|
|
26
|
-
try {
|
|
27
|
-
await fsPromises.access(checkPath);
|
|
28
|
-
return true;
|
|
29
|
-
} catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 列出可用的 skills
|
|
36
|
-
*/
|
|
37
|
-
async function listAvailableSkills() {
|
|
38
|
-
const skillsDir = getSkillsDir();
|
|
39
|
-
if (!(await pathExists(skillsDir))) {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const items = await fsPromises.readdir(skillsDir);
|
|
45
|
-
const skills = [];
|
|
46
|
-
|
|
47
|
-
for (const item of items) {
|
|
48
|
-
const skillPath = path.join(skillsDir, item);
|
|
49
|
-
const stat = await fsPromises.stat(skillPath);
|
|
50
|
-
if (stat.isDirectory()) {
|
|
51
|
-
skills.push(item);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return skills;
|
|
56
|
-
} catch {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 获取 skill 目录
|
|
63
|
-
*/
|
|
64
|
-
async function getSkillDir(skillName) {
|
|
65
|
-
const skillsDir = getSkillsDir();
|
|
66
|
-
const skillPath = path.join(skillsDir, skillName);
|
|
67
|
-
|
|
68
|
-
if (await pathExists(skillPath)) {
|
|
69
|
-
return skillPath;
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 递归复制目录
|
|
76
|
-
*/
|
|
77
|
-
async function copyDir(src, dest) {
|
|
78
|
-
await fsPromises.mkdir(dest, { recursive: true });
|
|
79
|
-
const entries = await fsPromises.readdir(src, { withFileTypes: true });
|
|
80
|
-
|
|
81
|
-
for (const entry of entries) {
|
|
82
|
-
const srcPath = path.join(src, entry.name);
|
|
83
|
-
const destPath = path.join(dest, entry.name);
|
|
84
|
-
|
|
85
|
-
if (entry.isDirectory()) {
|
|
86
|
-
await copyDir(srcPath, destPath);
|
|
87
|
-
} else {
|
|
88
|
-
await fsPromises.copyFile(srcPath, destPath);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* 删除目录
|
|
95
|
-
*/
|
|
96
|
-
async function removeDir(dirPath) {
|
|
97
|
-
if (await pathExists(dirPath)) {
|
|
98
|
-
await fsPromises.rm(dirPath, { recursive: true, force: true });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* 提示选择安装模式
|
|
104
|
-
*/
|
|
105
|
-
async function promptMode(action = 'install') {
|
|
106
|
-
const { mode } = await inquirer.prompt([{
|
|
107
|
-
type: 'list',
|
|
108
|
-
name: 'mode',
|
|
109
|
-
message: `选择 ${action} 位置:`,
|
|
110
|
-
choices: [
|
|
111
|
-
{
|
|
112
|
-
name: `全局 (Global) - 所有项目可用`,
|
|
113
|
-
value: 'global'
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
name: `本地 (Local) - 仅当前项目可用`,
|
|
117
|
-
value: 'local'
|
|
118
|
-
}
|
|
119
|
-
],
|
|
120
|
-
default: 'global'
|
|
121
|
-
}]);
|
|
122
|
-
|
|
123
|
-
return mode;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
*
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { getTarget, getGlobalPath, getLocalPath, getAllTargets, getTargetNames } = require('./skill-targets');
|
|
5
|
+
|
|
6
|
+
const fsPromises = fs.promises;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 获取包根目录
|
|
10
|
+
*/
|
|
11
|
+
function getPackageRoot() {
|
|
12
|
+
return path.join(__dirname, '..');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 获取 skills 目录
|
|
17
|
+
*/
|
|
18
|
+
function getSkillsDir() {
|
|
19
|
+
return path.join(getPackageRoot(), 'skills');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 检查路径是否存在
|
|
24
|
+
*/
|
|
25
|
+
async function pathExists(checkPath) {
|
|
26
|
+
try {
|
|
27
|
+
await fsPromises.access(checkPath);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 列出可用的 skills
|
|
36
|
+
*/
|
|
37
|
+
async function listAvailableSkills() {
|
|
38
|
+
const skillsDir = getSkillsDir();
|
|
39
|
+
if (!(await pathExists(skillsDir))) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const items = await fsPromises.readdir(skillsDir);
|
|
45
|
+
const skills = [];
|
|
46
|
+
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
const skillPath = path.join(skillsDir, item);
|
|
49
|
+
const stat = await fsPromises.stat(skillPath);
|
|
50
|
+
if (stat.isDirectory()) {
|
|
51
|
+
skills.push(item);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return skills;
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 获取 skill 目录
|
|
63
|
+
*/
|
|
64
|
+
async function getSkillDir(skillName) {
|
|
65
|
+
const skillsDir = getSkillsDir();
|
|
66
|
+
const skillPath = path.join(skillsDir, skillName);
|
|
67
|
+
|
|
68
|
+
if (await pathExists(skillPath)) {
|
|
69
|
+
return skillPath;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 递归复制目录
|
|
76
|
+
*/
|
|
77
|
+
async function copyDir(src, dest) {
|
|
78
|
+
await fsPromises.mkdir(dest, { recursive: true });
|
|
79
|
+
const entries = await fsPromises.readdir(src, { withFileTypes: true });
|
|
80
|
+
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const srcPath = path.join(src, entry.name);
|
|
83
|
+
const destPath = path.join(dest, entry.name);
|
|
84
|
+
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
await copyDir(srcPath, destPath);
|
|
87
|
+
} else {
|
|
88
|
+
await fsPromises.copyFile(srcPath, destPath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 删除目录
|
|
95
|
+
*/
|
|
96
|
+
async function removeDir(dirPath) {
|
|
97
|
+
if (await pathExists(dirPath)) {
|
|
98
|
+
await fsPromises.rm(dirPath, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 提示选择安装模式
|
|
104
|
+
*/
|
|
105
|
+
async function promptMode(action = 'install') {
|
|
106
|
+
const { mode } = await inquirer.prompt([{
|
|
107
|
+
type: 'list',
|
|
108
|
+
name: 'mode',
|
|
109
|
+
message: `选择 ${action} 位置:`,
|
|
110
|
+
choices: [
|
|
111
|
+
{
|
|
112
|
+
name: `全局 (Global) - 所有项目可用`,
|
|
113
|
+
value: 'global'
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: `本地 (Local) - 仅当前项目可用`,
|
|
117
|
+
value: 'local'
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
default: 'global'
|
|
121
|
+
}]);
|
|
122
|
+
|
|
123
|
+
return mode;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 执行单个 skill 安装
|
|
128
|
+
* @param {boolean|null} overwrite - true/false 直接覆盖/跳过,null 则询问
|
|
129
|
+
*/
|
|
130
|
+
async function installSingleSkill(skillName, skillDir, targetPath, mode, overwrite = null) {
|
|
131
|
+
// 检查是否已存在
|
|
132
|
+
const destPath = path.join(targetPath, skillName);
|
|
133
|
+
const exists = await pathExists(destPath);
|
|
134
|
+
|
|
135
|
+
if (exists) {
|
|
136
|
+
let shouldOverwrite = overwrite;
|
|
137
|
+
if (shouldOverwrite === null) {
|
|
138
|
+
const answer = await inquirer.prompt([{
|
|
139
|
+
type: 'confirm',
|
|
140
|
+
name: 'overwrite',
|
|
141
|
+
message: `Skill "${skillName}" 已存在,是否覆盖?`,
|
|
142
|
+
default: false
|
|
143
|
+
}]);
|
|
144
|
+
shouldOverwrite = answer.overwrite;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!shouldOverwrite) {
|
|
148
|
+
console.log(`⚠️ 跳过 ${skillName}`);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 删除旧的
|
|
153
|
+
await removeDir(destPath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 复制 skill 文件
|
|
157
|
+
console.log(`📦 正在安装 ${skillName}...`);
|
|
158
|
+
await copyDir(skillDir, destPath);
|
|
159
|
+
|
|
160
|
+
// 验证安装
|
|
161
|
+
if (await pathExists(destPath)) {
|
|
162
|
+
const modeText = mode === 'global' ? '全局' : '本地';
|
|
163
|
+
console.log(`✅ 成功 ${modeText} 安装 ${skillName}`);
|
|
164
|
+
return true;
|
|
165
|
+
} else {
|
|
166
|
+
console.log(`❌ ${skillName} 安装验证失败`);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 安装 skill
|
|
173
|
+
*/
|
|
174
|
+
async function installSkill(skillName, options = {}) {
|
|
175
|
+
try {
|
|
176
|
+
// 处理安装全部
|
|
177
|
+
const isAll = skillName === 'all' || options.all;
|
|
178
|
+
if (isAll) {
|
|
179
|
+
const availableSkills = await listAvailableSkills();
|
|
180
|
+
if (availableSkills.length === 0) {
|
|
181
|
+
console.log('⚠️ 没有可用的 skills');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 确定安装模式
|
|
186
|
+
let mode = 'global';
|
|
187
|
+
if (options.local) {
|
|
188
|
+
mode = 'local';
|
|
189
|
+
} else if (options.global) {
|
|
190
|
+
mode = 'global';
|
|
191
|
+
} else {
|
|
192
|
+
mode = await promptMode('install');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 确定目标
|
|
196
|
+
const target = options.target || 'kimi';
|
|
197
|
+
let targetPath;
|
|
198
|
+
if (mode === 'local') {
|
|
199
|
+
targetPath = getLocalPath(target);
|
|
200
|
+
} else {
|
|
201
|
+
targetPath = getGlobalPath(target);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!targetPath) {
|
|
205
|
+
console.log('❌ 无法确定安装路径');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await fsPromises.mkdir(targetPath, { recursive: true });
|
|
210
|
+
|
|
211
|
+
// 先确认覆盖策略
|
|
212
|
+
const overwriteMap = new Map();
|
|
213
|
+
const existingSkills = [];
|
|
214
|
+
for (const name of availableSkills) {
|
|
215
|
+
const destPath = path.join(targetPath, name);
|
|
216
|
+
if (await pathExists(destPath)) {
|
|
217
|
+
existingSkills.push(name);
|
|
218
|
+
} else {
|
|
219
|
+
overwriteMap.set(name, true);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let globalOverwrite = null; // null = 逐个确认, true = 全部覆盖, false = 全部跳过
|
|
224
|
+
if (existingSkills.length > 0) {
|
|
225
|
+
const { action } = await inquirer.prompt([{
|
|
226
|
+
type: 'list',
|
|
227
|
+
name: 'action',
|
|
228
|
+
message: `检测到 ${existingSkills.length} 个 skill 已存在,覆盖策略:`,
|
|
229
|
+
choices: [
|
|
230
|
+
{ name: '全部覆盖', value: 'all' },
|
|
231
|
+
{ name: '逐个确认', value: 'one-by-one' },
|
|
232
|
+
{ name: '全部跳过', value: 'skip' }
|
|
233
|
+
],
|
|
234
|
+
default: 'one-by-one'
|
|
235
|
+
}]);
|
|
236
|
+
|
|
237
|
+
if (action === 'all') {
|
|
238
|
+
globalOverwrite = true;
|
|
239
|
+
} else if (action === 'skip') {
|
|
240
|
+
globalOverwrite = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const name of existingSkills) {
|
|
245
|
+
if (globalOverwrite === true) {
|
|
246
|
+
overwriteMap.set(name, true);
|
|
247
|
+
} else if (globalOverwrite === false) {
|
|
248
|
+
overwriteMap.set(name, false);
|
|
249
|
+
} else {
|
|
250
|
+
const { overwrite } = await inquirer.prompt([{
|
|
251
|
+
type: 'confirm',
|
|
252
|
+
name: 'overwrite',
|
|
253
|
+
message: `Skill "${name}" 已存在,是否覆盖?`,
|
|
254
|
+
default: false
|
|
255
|
+
}]);
|
|
256
|
+
overwriteMap.set(name, overwrite);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 过滤掉选择不覆盖的
|
|
261
|
+
const skillsToInstall = availableSkills.filter(name => overwriteMap.get(name));
|
|
262
|
+
|
|
263
|
+
// 按每批 3 个并行安装
|
|
264
|
+
const BATCH_SIZE = 3;
|
|
265
|
+
let successCount = 0;
|
|
266
|
+
for (let i = 0; i < skillsToInstall.length; i += BATCH_SIZE) {
|
|
267
|
+
const batch = skillsToInstall.slice(i, i + BATCH_SIZE);
|
|
268
|
+
const results = await Promise.all(
|
|
269
|
+
batch.map(async (name) => {
|
|
270
|
+
const skillDir = await getSkillDir(name);
|
|
271
|
+
if (!skillDir) return false;
|
|
272
|
+
return installSingleSkill(name, skillDir, targetPath, mode, true);
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
successCount += results.filter(Boolean).length;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const total = availableSkills.length;
|
|
279
|
+
const skipped = total - skillsToInstall.length;
|
|
280
|
+
console.log();
|
|
281
|
+
console.log(`🎉 共安装 ${successCount}/${total} 个 skills${skipped > 0 ? `(跳过 ${skipped} 个)` : ''}`);
|
|
282
|
+
console.log();
|
|
283
|
+
console.log('⚠️ 重要: 请重启你的 AI agent 以加载新安装的 skills!');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 检查 skill 是否存在
|
|
288
|
+
const skillDir = await getSkillDir(skillName);
|
|
289
|
+
if (!skillDir) {
|
|
290
|
+
const availableSkills = await listAvailableSkills();
|
|
291
|
+
console.log(`❌ Skill "${skillName}" 不存在`);
|
|
292
|
+
console.log(`可用的 skills: ${availableSkills.join(', ') || '无'}`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 确定安装模式
|
|
297
|
+
let mode = 'global';
|
|
298
|
+
if (options.local) {
|
|
299
|
+
mode = 'local';
|
|
300
|
+
} else if (options.global) {
|
|
301
|
+
mode = 'global';
|
|
302
|
+
} else {
|
|
303
|
+
mode = await promptMode('install');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 确定目标
|
|
307
|
+
const target = options.target || 'kimi';
|
|
308
|
+
|
|
309
|
+
// 确定目标路径
|
|
310
|
+
let targetPath;
|
|
311
|
+
if (mode === 'local') {
|
|
312
|
+
targetPath = getLocalPath(target);
|
|
313
|
+
} else {
|
|
314
|
+
targetPath = getGlobalPath(target);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!targetPath) {
|
|
318
|
+
console.log('❌ 无法确定安装路径');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 确保目标目录存在
|
|
323
|
+
await fsPromises.mkdir(targetPath, { recursive: true });
|
|
324
|
+
|
|
325
|
+
const ok = await installSingleSkill(skillName, skillDir, targetPath, mode);
|
|
326
|
+
|
|
327
|
+
if (ok) {
|
|
328
|
+
const destPath = path.join(targetPath, skillName);
|
|
329
|
+
console.log(` 位置: ${destPath}`);
|
|
330
|
+
console.log();
|
|
331
|
+
console.log('⚠️ 重要: 请重启你的 AI agent 以加载新安装的 skill!');
|
|
332
|
+
console.log();
|
|
333
|
+
console.log('重启后,你可以在对话中:');
|
|
334
|
+
console.log(` • 直接询问关于 "${skillName}" 的内容`);
|
|
335
|
+
console.log(` • 使用 /skill:${skillName} 强制加载该 skill`);
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.log(`❌ 错误: ${error.message}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 卸载 skill
|
|
344
|
+
*/
|
|
345
|
+
async function uninstallSkill(skillName, options = {}) {
|
|
346
|
+
try {
|
|
347
|
+
// 确定安装模式
|
|
348
|
+
let mode = 'global';
|
|
349
|
+
if (options.local) {
|
|
350
|
+
mode = 'local';
|
|
351
|
+
} else if (options.global) {
|
|
352
|
+
mode = 'global';
|
|
353
|
+
} else {
|
|
354
|
+
mode = await promptMode('uninstall');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 确定目标
|
|
358
|
+
const target = options.target || 'kimi';
|
|
359
|
+
|
|
360
|
+
// 确定目标路径
|
|
361
|
+
let targetPath;
|
|
362
|
+
if (mode === 'local') {
|
|
363
|
+
targetPath = getLocalPath(target);
|
|
364
|
+
} else {
|
|
365
|
+
targetPath = getGlobalPath(target);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const skillPath = path.join(targetPath, skillName);
|
|
369
|
+
|
|
370
|
+
// 检查是否存在
|
|
371
|
+
if (!(await pathExists(skillPath))) {
|
|
372
|
+
console.log(`⚠️ Skill "${skillName}" 不存在于 ${skillPath}`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 确认卸载
|
|
377
|
+
const { confirm } = await inquirer.prompt([{
|
|
378
|
+
type: 'confirm',
|
|
379
|
+
name: 'confirm',
|
|
380
|
+
message: `确认卸载 "${skillName}"?`,
|
|
381
|
+
default: false
|
|
382
|
+
}]);
|
|
383
|
+
|
|
384
|
+
if (!confirm) {
|
|
385
|
+
console.log('⚠️ 已取消卸载');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 删除 skill
|
|
390
|
+
console.log(`🗑️ 正在卸载 ${skillName}...`);
|
|
391
|
+
await removeDir(skillPath);
|
|
392
|
+
|
|
393
|
+
console.log(`✅ 成功卸载 ${skillName}`);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
console.log(`❌ 错误: ${error.message}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* 列出可用的 skills
|
|
401
|
+
*/
|
|
402
|
+
async function listSkills() {
|
|
403
|
+
try {
|
|
404
|
+
const skills = await listAvailableSkills();
|
|
405
|
+
|
|
406
|
+
if (skills.length === 0) {
|
|
407
|
+
console.log('⚠️ 没有可用的 skills');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
console.log('📚 可用的 FastCar Skills:');
|
|
412
|
+
console.log();
|
|
413
|
+
|
|
414
|
+
for (const skill of skills) {
|
|
415
|
+
const skillDir = await getSkillDir(skill);
|
|
416
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
417
|
+
|
|
418
|
+
let description = '';
|
|
419
|
+
if (await pathExists(skillMdPath)) {
|
|
420
|
+
const content = await fsPromises.readFile(skillMdPath, 'utf-8');
|
|
421
|
+
const match = content.match(/description:\s*(.+)/);
|
|
422
|
+
if (match) {
|
|
423
|
+
description = match[1].trim();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
console.log(` • ${skill}`);
|
|
428
|
+
if (description) {
|
|
429
|
+
console.log(` ${description}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
console.log();
|
|
434
|
+
console.log('安装命令: fastcar-cli skill install <skill-name>');
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.log(`❌ 错误: ${error.message}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* 列出支持的目标 agents
|
|
442
|
+
*/
|
|
443
|
+
function listTargets() {
|
|
444
|
+
const targets = getAllTargets();
|
|
445
|
+
|
|
446
|
+
console.log('🤖 支持的 AI Agents:');
|
|
447
|
+
console.log();
|
|
448
|
+
|
|
449
|
+
for (const [key, config] of Object.entries(targets)) {
|
|
450
|
+
const globalPath = getGlobalPath(key);
|
|
451
|
+
const localPath = getLocalPath(key);
|
|
452
|
+
|
|
453
|
+
console.log(` • ${config.name} (${key})`);
|
|
454
|
+
console.log(` ${config.description}`);
|
|
455
|
+
console.log(` 全局: ${globalPath}`);
|
|
456
|
+
console.log(` 本地: ${localPath}`);
|
|
457
|
+
console.log();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* 初始化项目级配置
|
|
463
|
+
*/
|
|
464
|
+
async function initSkill(options = {}) {
|
|
465
|
+
try {
|
|
466
|
+
const target = options.target || 'kimi';
|
|
467
|
+
const localPath = getLocalPath(target);
|
|
468
|
+
|
|
469
|
+
console.log(`🔧 正在初始化 ${target} 配置...`);
|
|
470
|
+
|
|
471
|
+
if (await pathExists(localPath)) {
|
|
472
|
+
console.log(`⚠️ 目录已存在: ${localPath}`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
await fsPromises.mkdir(localPath, { recursive: true });
|
|
477
|
+
|
|
478
|
+
console.log(`✅ 已创建 ${localPath}`);
|
|
479
|
+
console.log();
|
|
480
|
+
console.log('下一步:');
|
|
481
|
+
console.log(` fastcar-cli skill install <skill-name> --local`);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.log(`❌ 错误: ${error.message}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
module.exports = {
|
|
488
|
+
installSkill,
|
|
489
|
+
uninstallSkill,
|
|
490
|
+
listSkills,
|
|
491
|
+
listTargets,
|
|
492
|
+
initSkill
|
|
493
|
+
};
|