foliko 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/.claude/settings.local.json +30 -0
- package/22.txt +10 -0
- package/README.md +218 -0
- package/SPEC.md +452 -0
- package/cli/bin/foliko.js +12 -0
- package/cli/src/commands/chat.js +75 -0
- package/cli/src/index.js +64 -0
- package/cli/src/ui/chat-ui.js +272 -0
- package/cli/src/utils/ansi.js +40 -0
- package/cli/src/utils/markdown.js +296 -0
- package/docs/quick-reference.md +131 -0
- package/docs/user-manual.md +1205 -0
- package/examples/basic.js +110 -0
- package/examples/bootstrap.js +93 -0
- package/examples/mcp-example.js +53 -0
- package/examples/skill-example.js +49 -0
- package/examples/workflow.js +158 -0
- package/package.json +36 -0
- package/plugins/ai-plugin.js +89 -0
- package/plugins/audit-plugin.js +187 -0
- package/plugins/default-plugins.js +412 -0
- package/plugins/file-system-plugin.js +344 -0
- package/plugins/install-plugin.js +93 -0
- package/plugins/python-executor-plugin.js +331 -0
- package/plugins/rules-plugin.js +292 -0
- package/plugins/scheduler-plugin.js +426 -0
- package/plugins/session-plugin.js +343 -0
- package/plugins/shell-executor-plugin.js +196 -0
- package/plugins/storage-plugin.js +237 -0
- package/plugins/subagent-plugin.js +395 -0
- package/plugins/think-plugin.js +329 -0
- package/plugins/tools-plugin.js +114 -0
- package/skills/mcp-usage/SKILL.md +198 -0
- package/skills/vb-agent-dev/AGENTS.md +162 -0
- package/skills/vb-agent-dev/SKILL.md +370 -0
- package/src/capabilities/index.js +11 -0
- package/src/capabilities/skill-manager.js +319 -0
- package/src/capabilities/workflow-engine.js +401 -0
- package/src/core/agent-chat.js +311 -0
- package/src/core/agent.js +573 -0
- package/src/core/framework.js +255 -0
- package/src/core/index.js +19 -0
- package/src/core/plugin-base.js +205 -0
- package/src/core/plugin-manager.js +392 -0
- package/src/core/provider.js +108 -0
- package/src/core/tool-registry.js +134 -0
- package/src/core/tool-router.js +216 -0
- package/src/executors/executor-base.js +58 -0
- package/src/executors/mcp-executor.js +728 -0
- package/src/index.js +37 -0
- package/src/utils/event-emitter.js +97 -0
- package/test-chat.js +129 -0
- package/test-mcp.js +79 -0
- package/test-reload.js +61 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileSystem 插件
|
|
3
|
+
* 提供文件系统操作的常用工具
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { Plugin } = require('../src/core/plugin-base')
|
|
7
|
+
|
|
8
|
+
class FileSystemPlugin extends Plugin {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
super()
|
|
11
|
+
this.name = 'file-system'
|
|
12
|
+
this.version = '1.0.0'
|
|
13
|
+
this.description = '文件系统工具插件'
|
|
14
|
+
this.priority = 5
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
install(framework) {
|
|
18
|
+
const { z } = require('zod')
|
|
19
|
+
const fs = require('fs')
|
|
20
|
+
const path = require('path')
|
|
21
|
+
const { exec } = require('child_process')
|
|
22
|
+
|
|
23
|
+
// 读取目录
|
|
24
|
+
framework.registerTool({
|
|
25
|
+
name: 'read_directory',
|
|
26
|
+
description: '读取目录内容,列出目录中的文件和子目录',
|
|
27
|
+
inputSchema: z.object({
|
|
28
|
+
path: z.string().optional().describe('目录路径'),
|
|
29
|
+
dirPath: z.string().optional().describe('目录路径(同path)'),
|
|
30
|
+
recursive: z.boolean().optional().describe('是否递归')
|
|
31
|
+
}),
|
|
32
|
+
execute: async (args, framework) => {
|
|
33
|
+
const dirPath = args.path || args.dirPath || '.'
|
|
34
|
+
const recursive = args.recursive || false
|
|
35
|
+
try {
|
|
36
|
+
const items = []
|
|
37
|
+
const readDir = (currentPath, depth = 0) => {
|
|
38
|
+
if (depth > 3 && recursive) return
|
|
39
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const fullPath = path.join(currentPath, entry.name)
|
|
42
|
+
const relativePath = path.relative(process.cwd(), fullPath)
|
|
43
|
+
if (relativePath.includes('node_modules') || relativePath.includes('.git')) {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
items.push({
|
|
47
|
+
name: entry.name,
|
|
48
|
+
path: relativePath,
|
|
49
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
50
|
+
size: entry.isFile() ? fs.statSync(fullPath).size : null
|
|
51
|
+
})
|
|
52
|
+
if (entry.isDirectory() && recursive && depth < 3) {
|
|
53
|
+
readDir(fullPath, depth + 1)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
readDir(dirPath)
|
|
58
|
+
return { success: true, dirPath, items: items.slice(0, 100), total: items.length }
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return { success: false, error: error.message }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// 创建目录
|
|
66
|
+
framework.registerTool({
|
|
67
|
+
name: 'create_directory',
|
|
68
|
+
description: '创建新目录',
|
|
69
|
+
inputSchema: z.object({
|
|
70
|
+
path: z.string().optional().describe('目录路径'),
|
|
71
|
+
dirPath: z.string().optional().describe('目录路径(同path)')
|
|
72
|
+
}),
|
|
73
|
+
execute: async (args, framework) => {
|
|
74
|
+
const dirPath = args.path || args.dirPath
|
|
75
|
+
try {
|
|
76
|
+
fs.mkdirSync(dirPath, { recursive: true })
|
|
77
|
+
return { success: true, message: `目录已创建: ${dirPath}` }
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return { success: false, error: error.message }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// 读取文件
|
|
85
|
+
framework.registerTool({
|
|
86
|
+
name: 'read_file',
|
|
87
|
+
description: '读取文件内容',
|
|
88
|
+
inputSchema: z.object({
|
|
89
|
+
path: z.string().optional().describe('文件路径'),
|
|
90
|
+
filePath: z.string().optional().describe('文件路径(同path)'),
|
|
91
|
+
encoding: z.enum(['utf8', 'base64', 'binary']).optional().describe('文件编码,默认 utf8'),
|
|
92
|
+
lines: z.number().optional().describe('只读取前 N 行')
|
|
93
|
+
}),
|
|
94
|
+
execute: async (args, framework) => {
|
|
95
|
+
const filePath = args.path || args.filePath
|
|
96
|
+
const encoding = args.encoding || 'utf8'
|
|
97
|
+
const lines = args.lines
|
|
98
|
+
try {
|
|
99
|
+
if (!fs.existsSync(filePath)) {
|
|
100
|
+
return { success: false, error: '文件不存在' }
|
|
101
|
+
}
|
|
102
|
+
const stat = fs.statSync(filePath)
|
|
103
|
+
if (stat.size > 1024 * 1024) {
|
|
104
|
+
return { success: false, error: '文件太大,超过 1MB' }
|
|
105
|
+
}
|
|
106
|
+
let content
|
|
107
|
+
if (lines) {
|
|
108
|
+
const fileContent = fs.readFileSync(filePath, 'utf8')
|
|
109
|
+
const allLines = fileContent.split('\n')
|
|
110
|
+
content = allLines.slice(0, lines).join('\n')
|
|
111
|
+
} else {
|
|
112
|
+
content = fs.readFileSync(filePath, encoding)
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
filePath,
|
|
117
|
+
content,
|
|
118
|
+
size: stat.size,
|
|
119
|
+
lines: lines ? null : content.split('\n').length
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return { success: false, error: error.message }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// 写入文件
|
|
128
|
+
framework.registerTool({
|
|
129
|
+
name: 'write_file',
|
|
130
|
+
description: '创建或写入文件内容',
|
|
131
|
+
inputSchema: z.object({
|
|
132
|
+
path: z.string().optional().describe('文件路径'),
|
|
133
|
+
filePath: z.string().optional().describe('文件路径(同path)'),
|
|
134
|
+
content: z.string().describe('文件内容')
|
|
135
|
+
}),
|
|
136
|
+
execute: async (args, framework) => {
|
|
137
|
+
const filePath = args.path || args.filePath
|
|
138
|
+
const content = args.content
|
|
139
|
+
const encoding = args.encoding || 'utf8'
|
|
140
|
+
try {
|
|
141
|
+
const dir = path.dirname(filePath)
|
|
142
|
+
if (!fs.existsSync(dir)) {
|
|
143
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
144
|
+
}
|
|
145
|
+
fs.writeFileSync(filePath, content, encoding)
|
|
146
|
+
return { success: true, message: `文件已写入: ${filePath}`, filePath, size: content.length }
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return { success: false, error: error.message }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// 删除文件
|
|
154
|
+
framework.registerTool({
|
|
155
|
+
name: 'delete_file',
|
|
156
|
+
description: '删除文件或目录',
|
|
157
|
+
inputSchema: z.object({
|
|
158
|
+
path: z.string().optional().describe('文件或目录路径'),
|
|
159
|
+
targetPath: z.string().optional().describe('文件或目录路径(同path)'),
|
|
160
|
+
recursive: z.boolean().optional().describe('递归删除目录')
|
|
161
|
+
}),
|
|
162
|
+
execute: async (args, framework) => {
|
|
163
|
+
const targetPath = args.path || args.targetPath
|
|
164
|
+
const recursive = args.recursive || false
|
|
165
|
+
try {
|
|
166
|
+
if (!fs.existsSync(targetPath)) {
|
|
167
|
+
return { success: false, error: '目标不存在' }
|
|
168
|
+
}
|
|
169
|
+
const stat = fs.statSync(targetPath)
|
|
170
|
+
if (stat.isDirectory()) {
|
|
171
|
+
if (recursive) {
|
|
172
|
+
fs.rmSync(targetPath, { recursive: true, force: true })
|
|
173
|
+
} else {
|
|
174
|
+
fs.rmdirSync(targetPath)
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
fs.unlinkSync(targetPath)
|
|
178
|
+
}
|
|
179
|
+
return { success: true, message: `已删除: ${targetPath}` }
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return { success: false, error: error.message }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// 修改文件(替换文本)
|
|
187
|
+
framework.registerTool({
|
|
188
|
+
name: 'modify_file',
|
|
189
|
+
description: '修改文件内容(替换文本)',
|
|
190
|
+
inputSchema: z.object({
|
|
191
|
+
path: z.string().optional().describe('文件路径'),
|
|
192
|
+
filePath: z.string().optional().describe('文件路径(同path)'),
|
|
193
|
+
find: z.string().describe('要查找的文本'),
|
|
194
|
+
replace: z.string().describe('替换后的文本'),
|
|
195
|
+
replaceAll: z.boolean().optional().describe('替换所有匹配')
|
|
196
|
+
}),
|
|
197
|
+
execute: async (args, framework) => {
|
|
198
|
+
const filePath = args.path || args.filePath
|
|
199
|
+
const find = args.find
|
|
200
|
+
const replace = args.replace
|
|
201
|
+
const replaceAll = args.replaceAll || false
|
|
202
|
+
try {
|
|
203
|
+
if (!fs.existsSync(filePath)) {
|
|
204
|
+
return { success: false, error: '文件不存在' }
|
|
205
|
+
}
|
|
206
|
+
let content = fs.readFileSync(filePath, 'utf8')
|
|
207
|
+
if (replaceAll) {
|
|
208
|
+
content = content.split(find).join(replace)
|
|
209
|
+
} else {
|
|
210
|
+
content = content.replace(find, replace)
|
|
211
|
+
}
|
|
212
|
+
fs.writeFileSync(filePath, content, 'utf8')
|
|
213
|
+
return { success: true, message: `文件已修改: ${filePath}`, filePath }
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return { success: false, error: error.message }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// 搜索文件
|
|
221
|
+
framework.registerTool({
|
|
222
|
+
name: 'search_file',
|
|
223
|
+
description: '在文件中搜索文本',
|
|
224
|
+
inputSchema: z.object({
|
|
225
|
+
query: z.string().optional().describe('搜索关键词'),
|
|
226
|
+
pattern: z.string().optional().describe('搜索模式'),
|
|
227
|
+
searchText: z.string().optional().describe('搜索文本'),
|
|
228
|
+
path: z.string().optional().describe('搜索目录'),
|
|
229
|
+
dirPath: z.string().optional().describe('搜索目录(同path)'),
|
|
230
|
+
fileType: z.string().optional().describe('文件类型过滤'),
|
|
231
|
+
maxResults: z.number().optional().describe('最大结果数')
|
|
232
|
+
}),
|
|
233
|
+
execute: async (args, framework) => {
|
|
234
|
+
const pattern = args.query || args.pattern || args.searchText || ''
|
|
235
|
+
const dirPath = args.path || args.dirPath || '.'
|
|
236
|
+
const fileType = args.fileType
|
|
237
|
+
const maxResults = args.maxResults || 50
|
|
238
|
+
try {
|
|
239
|
+
const results = []
|
|
240
|
+
const regex = new RegExp(pattern, 'gi')
|
|
241
|
+
const searchDir = (currentPath, depth = 0) => {
|
|
242
|
+
if (depth > 5 || results.length >= maxResults) return
|
|
243
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
244
|
+
for (const entry of entries) {
|
|
245
|
+
if (results.length >= maxResults) break
|
|
246
|
+
const fullPath = path.join(currentPath, entry.name)
|
|
247
|
+
const relativePath = path.relative(process.cwd(), fullPath)
|
|
248
|
+
if (relativePath.includes('node_modules') || relativePath.includes('.git') ||
|
|
249
|
+
relativePath.includes('dist') || relativePath.includes('build')) {
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
if (entry.isDirectory()) {
|
|
253
|
+
searchDir(fullPath, depth + 1)
|
|
254
|
+
} else if (entry.isFile()) {
|
|
255
|
+
if (fileType && !entry.name.endsWith(fileType)) continue
|
|
256
|
+
const ext = path.extname(entry.name)
|
|
257
|
+
if (['.jpg', '.png', '.gif', '.exe', '.dll', '.zip'].includes(ext)) continue
|
|
258
|
+
try {
|
|
259
|
+
const content = fs.readFileSync(fullPath, 'utf8')
|
|
260
|
+
const lines = content.split('\n')
|
|
261
|
+
for (let i = 0; i < lines.length; i++) {
|
|
262
|
+
if (regex.test(lines[i])) {
|
|
263
|
+
results.push({ file: relativePath, line: i + 1, content: lines[i].substring(0, 100) })
|
|
264
|
+
if (results.length >= maxResults) break
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
regex.lastIndex = 0
|
|
268
|
+
} catch (e) { }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
searchDir(dirPath)
|
|
273
|
+
return { success: true, pattern, results: results.slice(0, maxResults), total: results.length }
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return { success: false, error: error.message }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
// 执行命令
|
|
281
|
+
framework.registerTool({
|
|
282
|
+
name: 'execute_command',
|
|
283
|
+
description: '执行终端命令',
|
|
284
|
+
inputSchema: z.object({
|
|
285
|
+
cmd: z.string().optional().describe('要执行的命令'),
|
|
286
|
+
command: z.string().optional().describe('要执行的命令(同cmd)'),
|
|
287
|
+
run: z.string().optional().describe('要执行的命令(同cmd)'),
|
|
288
|
+
cwd: z.string().optional().describe('工作目录'),
|
|
289
|
+
timeout: z.number().optional().describe('超时时间(ms)')
|
|
290
|
+
}),
|
|
291
|
+
execute: async (args, framework) => {
|
|
292
|
+
const command = args.cmd || args.command || args.run
|
|
293
|
+
const cwd = args.cwd || process.cwd()
|
|
294
|
+
const timeout = args.timeout || 30000
|
|
295
|
+
return new Promise((resolve) => {
|
|
296
|
+
const startTime = Date.now()
|
|
297
|
+
exec(command, { cwd, timeout }, (error, stdout, stderr) => {
|
|
298
|
+
const duration = Date.now() - startTime
|
|
299
|
+
if (error) {
|
|
300
|
+
resolve({ success: false, command, error: error.message, stderr, duration })
|
|
301
|
+
} else {
|
|
302
|
+
resolve({
|
|
303
|
+
success: true,
|
|
304
|
+
command,
|
|
305
|
+
stdout: stdout.substring(0, 10000),
|
|
306
|
+
stderr: stderr.substring(0, 1000),
|
|
307
|
+
duration
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
// 获取北京时间
|
|
316
|
+
framework.registerTool({
|
|
317
|
+
name: 'get_time',
|
|
318
|
+
description: '获取当前的时间,需要获取日期和时间的时候需要调用该接口',
|
|
319
|
+
inputSchema: z.object({}),
|
|
320
|
+
execute: async (args, framework) => {
|
|
321
|
+
const now = new Date()
|
|
322
|
+
const beijingTime = new Date(now.getTime() + (8 * 60 * 60 * 1000))
|
|
323
|
+
return {
|
|
324
|
+
success: true,
|
|
325
|
+
beijingTime: beijingTime.toISOString().replace('T', ' ').substring(0, 19),
|
|
326
|
+
timestamp: now.getTime(),
|
|
327
|
+
timezone: 'Asia/Shanghai (UTC+8)',
|
|
328
|
+
formatted: {
|
|
329
|
+
year: beijingTime.getUTCFullYear(),
|
|
330
|
+
month: String(beijingTime.getUTCMonth() + 1).padStart(2, '0'),
|
|
331
|
+
day: String(beijingTime.getUTCDate()).padStart(2, '0'),
|
|
332
|
+
hour: String(beijingTime.getUTCHours()).padStart(2, '0'),
|
|
333
|
+
minute: String(beijingTime.getUTCMinutes()).padStart(2, '0'),
|
|
334
|
+
second: String(beijingTime.getUTCSeconds()).padStart(2, '0')
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
return this
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = { FileSystemPlugin }
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InstallPlugin - npm 包安装工具
|
|
3
|
+
* 自动安装缺少的 node 模块到 .agent 目录
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { execSync } = require('child_process')
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const { Plugin } = require('../src/core/plugin-base')
|
|
10
|
+
const { z } = require('zod')
|
|
11
|
+
|
|
12
|
+
class InstallPlugin extends Plugin {
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
super()
|
|
15
|
+
this.name = 'install'
|
|
16
|
+
this.version = '1.0.0'
|
|
17
|
+
this.description = '自动安装 npm 包到 .agent 目录'
|
|
18
|
+
|
|
19
|
+
this._agentDir = config.agentDir || '.agent'
|
|
20
|
+
this._nodeModulesDir = null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
install(framework) {
|
|
24
|
+
this._framework = framework
|
|
25
|
+
return this
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
start(framework) {
|
|
29
|
+
// 确保 node_modules 目录存在
|
|
30
|
+
this._nodeModulesDir = path.join(this._agentDir, 'node_modules')
|
|
31
|
+
if (!fs.existsSync(this._nodeModulesDir)) {
|
|
32
|
+
fs.mkdirSync(this._nodeModulesDir, { recursive: true })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 注册 install 工具
|
|
36
|
+
framework.registerTool({
|
|
37
|
+
name: 'install',
|
|
38
|
+
description: '安装 npm 包到 .agent 目录,用于解决插件依赖缺失问题',
|
|
39
|
+
inputSchema: z.object({
|
|
40
|
+
package: z.string().describe('包名,如 lodash 或 lodash@4.17.21')
|
|
41
|
+
}),
|
|
42
|
+
execute: async (args) => {
|
|
43
|
+
const { package: packageName } = args
|
|
44
|
+
return this._installPackage(packageName)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return this
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_installPackage(packageName) {
|
|
52
|
+
try {
|
|
53
|
+
console.log(`[InstallPlugin] Installing ${packageName} to ${this._nodeModulesDir}...`)
|
|
54
|
+
|
|
55
|
+
// 使用 npm install 安装到指定目录
|
|
56
|
+
// --prefix 确保安装到 .agent/node_modules
|
|
57
|
+
execSync(`npm install ${packageName} --prefix "${this._agentDir}"`, {
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
cwd: this._agentDir
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
console.log(`[InstallPlugin] Successfully installed ${packageName}`)
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
message: `Successfully installed ${packageName}`
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
error: `Failed to install ${packageName}: ${err.message}`
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 获取 node_modules 路径,供其他插件使用
|
|
78
|
+
*/
|
|
79
|
+
getNodeModulesDir() {
|
|
80
|
+
return this._nodeModulesDir
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 添加到 module.paths(让 require 能找到)
|
|
85
|
+
*/
|
|
86
|
+
addToModulePaths() {
|
|
87
|
+
if (this._nodeModulesDir && !module.paths.includes(this._nodeModulesDir)) {
|
|
88
|
+
module.paths.unshift(this._nodeModulesDir)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { InstallPlugin }
|