clawflowbang 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/.eslintrc.json +38 -0
- package/README.md +1 -0
- package/__tests__/config-manager.test.js +52 -0
- package/__tests__/cron-format.test.js +26 -0
- package/__tests__/cron-manager.local.test.js +65 -0
- package/__tests__/openclaw-cli.test.js +51 -0
- package/bin/clawflowhub.js +125 -0
- package/docs/clawhub-package-format.md +179 -0
- package/examples/npm-package-example/package.json +53 -0
- package/package.json +45 -0
- package/src/commands/cron.js +123 -0
- package/src/commands/init.js +112 -0
- package/src/commands/install.js +38 -0
- package/src/commands/list.js +77 -0
- package/src/commands/remove.js +27 -0
- package/src/commands/search.js +61 -0
- package/src/commands/status.js +68 -0
- package/src/core/ConfigManager.js +295 -0
- package/src/core/CronFormat.js +68 -0
- package/src/core/CronManager.js +512 -0
- package/src/core/Installer.js +352 -0
- package/src/core/NPMRegistry.js +285 -0
- package/src/core/OpenClawCLI.js +265 -0
- package/src/core/PackageResolver.js +94 -0
- package/src/core/Registry.js +400 -0
- package/src/index.js +91 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installer - จัดการการติดตั้งและถอนการติดตั้ง packages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const ora = require('ora');
|
|
9
|
+
const inquirer = require('inquirer');
|
|
10
|
+
const OpenClawCLI = require('./OpenClawCLI');
|
|
11
|
+
|
|
12
|
+
class Installer {
|
|
13
|
+
constructor(configManager, registry) {
|
|
14
|
+
this.configManager = configManager;
|
|
15
|
+
this.registry = registry;
|
|
16
|
+
this.cronManager = null; // จะ set จากภายนอก
|
|
17
|
+
this.openclawCLI = new OpenClawCLI(configManager);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setCronManager(cronManager) {
|
|
21
|
+
this.cronManager = cronManager;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* ติดตั้ง package
|
|
26
|
+
*/
|
|
27
|
+
async install(packageName, options = {}) {
|
|
28
|
+
const { global = false, cron: withCron = true, dryRun = false } = options;
|
|
29
|
+
|
|
30
|
+
// ตรวจสอบว่า package มีอยู่หรือไม่ (รองรับทั้ง built-in และ npm)
|
|
31
|
+
const pkg = await this.registry.getPackage(packageName);
|
|
32
|
+
if (!pkg) {
|
|
33
|
+
throw new Error(`Package "${packageName}" ไม่พบใน registry (ลองตรวจสอบชื่อ package หรือเชื่อมต่ออินเทอร์เน็ต)`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ตรวจสอบว่าติดตั้งแล้วหรือยัง
|
|
37
|
+
const installed = this.configManager.getInstalledPackages();
|
|
38
|
+
if (installed[packageName]) {
|
|
39
|
+
console.log(chalk.yellow(`⚠️ Package "${packageName}" ติดตั้งแล้ว`));
|
|
40
|
+
const { reinstall } = await inquirer.prompt([{
|
|
41
|
+
type: 'confirm',
|
|
42
|
+
name: 'reinstall',
|
|
43
|
+
message: 'ต้องการติดตั้งใหม่หรือไม่?',
|
|
44
|
+
default: false,
|
|
45
|
+
}]);
|
|
46
|
+
if (!reinstall) {
|
|
47
|
+
return { success: false, reason: 'already_installed' };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.cyan(`📦 กำลังติดตั้ง ${chalk.bold(packageName)}...\n`));
|
|
52
|
+
|
|
53
|
+
// Dry run - แสดงเฉพาะข้อมูล
|
|
54
|
+
if (dryRun) {
|
|
55
|
+
this.showDryRunInfo(pkg, withCron);
|
|
56
|
+
return { success: true, dryRun: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const spinner = ora('กำลังดำเนินการ...').start();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// 1. ติดตั้ง skills
|
|
63
|
+
spinner.text = 'กำลังติดตั้ง skills...';
|
|
64
|
+
await this.installSkills(pkg.skills, global);
|
|
65
|
+
|
|
66
|
+
// 2. ตั้งค่า config
|
|
67
|
+
spinner.text = 'กำลังตั้งค่า configuration...';
|
|
68
|
+
await this.setupConfig(packageName, pkg.config);
|
|
69
|
+
|
|
70
|
+
// 3. ตั้ง cronjobs (ถ้าเปิดใช้งาน)
|
|
71
|
+
let crons = [];
|
|
72
|
+
if (withCron && pkg.crons && pkg.crons.length > 0) {
|
|
73
|
+
spinner.text = 'กำลังตั้ง cronjobs...';
|
|
74
|
+
crons = await this.setupCrons(pkg.crons);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 4. บันทึกว่าติดตั้งแล้ว
|
|
78
|
+
spinner.text = 'กำลังบันทึกข้อมูล...';
|
|
79
|
+
this.configManager.addInstalledPackage(packageName, {
|
|
80
|
+
version: pkg.version,
|
|
81
|
+
skills: pkg.skills.map(s => s.name),
|
|
82
|
+
crons: crons.map(c => c.id),
|
|
83
|
+
configPath: this.getPackageConfigPath(packageName),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
spinner.succeed(chalk.green(`ติดตั้ง ${packageName} เสร็จสมบูรณ์!`));
|
|
87
|
+
|
|
88
|
+
// แสดงข้อมูลสรุป
|
|
89
|
+
this.showInstallSummary(pkg, crons);
|
|
90
|
+
|
|
91
|
+
// แสดง post-install message
|
|
92
|
+
if (pkg.postInstall) {
|
|
93
|
+
console.log(chalk.yellow('\n📋 ขั้นตอนถัดไป:'));
|
|
94
|
+
console.log(chalk.gray(` ${pkg.postInstall}`));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
package: packageName,
|
|
100
|
+
skills: pkg.skills.map(s => s.name),
|
|
101
|
+
crons: crons,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
} catch (error) {
|
|
105
|
+
spinner.fail(chalk.red(`ติดตั้งไม่สำเร็จ: ${error.message}`));
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* ติดตั้ง skills
|
|
112
|
+
*/
|
|
113
|
+
async installSkills(skills, global = false) {
|
|
114
|
+
for (const skill of skills) {
|
|
115
|
+
console.log(chalk.gray(` → ติดตั้ง skill: ${skill.name}@${skill.version}`));
|
|
116
|
+
|
|
117
|
+
// ติดตั้ง skill ผ่าน OpenClaw
|
|
118
|
+
// ในโลกจริงจะเรียก OpenClaw API หรือ CLI
|
|
119
|
+
await this.installSkillToOpenClaw(skill, global);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* ติดตั้ง skill ไปยัง OpenClaw
|
|
125
|
+
*/
|
|
126
|
+
async installSkillToOpenClaw(skill, global = false) {
|
|
127
|
+
void global;
|
|
128
|
+
const skillsPath = this.configManager.getSkillsPath();
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await this.openclawCLI.installSkill(skill.name, skill.version, skillsPath);
|
|
132
|
+
|
|
133
|
+
// ตรวจสอบหลังติดตั้ง (best effort)
|
|
134
|
+
await this.openclawCLI.verifySkill(skill.name).catch(() => null);
|
|
135
|
+
|
|
136
|
+
} catch (error) {
|
|
137
|
+
try {
|
|
138
|
+
const cloned = await this.openclawCLI.installSkillFromGit(skill, skillsPath);
|
|
139
|
+
console.log(chalk.yellow(` ↳ fallback git clone สำเร็จ: ${cloned.repository}`));
|
|
140
|
+
return;
|
|
141
|
+
} catch (gitError) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`ไม่สามารถติดตั้ง skill "${skill.name}" ได้ทั้งจาก clawhub และ git clone\n` +
|
|
144
|
+
`clawhub: ${error.message}\n` +
|
|
145
|
+
`git: ${gitError.message}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* ตั้งค่า config สำหรับ package
|
|
153
|
+
*/
|
|
154
|
+
async setupConfig(packageName, configSchema) {
|
|
155
|
+
if (!configSchema) return;
|
|
156
|
+
|
|
157
|
+
const configPath = this.getPackageConfigPath(packageName);
|
|
158
|
+
const existingConfig = fs.existsSync(configPath)
|
|
159
|
+
? fs.readJsonSync(configPath)
|
|
160
|
+
: {};
|
|
161
|
+
|
|
162
|
+
const newConfig = {};
|
|
163
|
+
|
|
164
|
+
for (const [skillName, schema] of Object.entries(configSchema)) {
|
|
165
|
+
newConfig[skillName] = {};
|
|
166
|
+
|
|
167
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
168
|
+
if (value.env) {
|
|
169
|
+
// ดึงค่าจาก environment variable
|
|
170
|
+
const envValue = process.env[value.env];
|
|
171
|
+
if (envValue) {
|
|
172
|
+
newConfig[skillName][key] = envValue;
|
|
173
|
+
} else if (value.required && !existingConfig[skillName]?.[key]) {
|
|
174
|
+
// ถ้าไม่มีใน env และจำเป็นต้องมี
|
|
175
|
+
const answer = await inquirer.prompt([{
|
|
176
|
+
type: 'input',
|
|
177
|
+
name: key,
|
|
178
|
+
message: `กรุณากรอก ${key} สำหรับ ${skillName} (หรือตั้งค่า ${value.env}):`,
|
|
179
|
+
validate: (input) => input.length > 0 || 'จำเป็นต้องกรอก',
|
|
180
|
+
}]);
|
|
181
|
+
newConfig[skillName][key] = answer[key];
|
|
182
|
+
}
|
|
183
|
+
} else if (value.default !== undefined) {
|
|
184
|
+
newConfig[skillName][key] = existingConfig[skillName]?.[key] ?? value.default;
|
|
185
|
+
} else if (value.required && !existingConfig[skillName]?.[key]) {
|
|
186
|
+
const answer = await inquirer.prompt([{
|
|
187
|
+
type: 'input',
|
|
188
|
+
name: key,
|
|
189
|
+
message: `กรุณากรอก ${key} สำหรับ ${skillName}:`,
|
|
190
|
+
validate: (input) => input.length > 0 || 'จำเป็นต้องกรอก',
|
|
191
|
+
}]);
|
|
192
|
+
newConfig[skillName][key] = answer[key];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// รวมกับ config เดิม
|
|
198
|
+
const mergedConfig = { ...existingConfig, ...newConfig };
|
|
199
|
+
fs.writeJsonSync(configPath, mergedConfig, { spaces: 2 });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* ตั้งค่า cronjobs
|
|
204
|
+
*/
|
|
205
|
+
async setupCrons(cronsConfig) {
|
|
206
|
+
const crons = [];
|
|
207
|
+
|
|
208
|
+
for (const cronConfig of cronsConfig) {
|
|
209
|
+
const cronInfo = await this.cronManager.add(
|
|
210
|
+
cronConfig.skill,
|
|
211
|
+
cronConfig.schedule,
|
|
212
|
+
cronConfig.params,
|
|
213
|
+
cronConfig.description
|
|
214
|
+
);
|
|
215
|
+
crons.push(cronInfo);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return crons;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* ถอนการติดตั้ง package
|
|
223
|
+
*/
|
|
224
|
+
async remove(packageName, options = {}) {
|
|
225
|
+
const { keepConfig = false } = options;
|
|
226
|
+
|
|
227
|
+
const installed = this.configManager.getInstalledPackages();
|
|
228
|
+
if (!installed[packageName]) {
|
|
229
|
+
console.log(chalk.yellow(`⚠️ Package "${packageName}" ไม่ได้ถูกติดตั้ง`));
|
|
230
|
+
return { success: false, reason: 'not_installed' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log(chalk.cyan(`🗑️ กำลังถอนการติดตั้ง ${chalk.bold(packageName)}...`));
|
|
234
|
+
|
|
235
|
+
const spinner = ora('กำลังดำเนินการ...').start();
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const pkgInfo = installed[packageName];
|
|
239
|
+
|
|
240
|
+
// 1. ลบ cronjobs
|
|
241
|
+
if (pkgInfo.crons && pkgInfo.crons.length > 0) {
|
|
242
|
+
spinner.text = 'กำลังลบ cronjobs...';
|
|
243
|
+
for (const cronId of pkgInfo.crons) {
|
|
244
|
+
await this.cronManager.remove(cronId);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 2. ลบ skills
|
|
249
|
+
if (pkgInfo.skills && pkgInfo.skills.length > 0) {
|
|
250
|
+
spinner.text = 'กำลังลบ skills...';
|
|
251
|
+
for (const skillName of pkgInfo.skills) {
|
|
252
|
+
await this.removeSkillFromOpenClaw(skillName);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 3. ลบ config (ถ้าไม่ได้ระบุให้เก็บไว้)
|
|
257
|
+
if (!keepConfig) {
|
|
258
|
+
spinner.text = 'กำลังลบ config...';
|
|
259
|
+
const configPath = this.getPackageConfigPath(packageName);
|
|
260
|
+
if (fs.existsSync(configPath)) {
|
|
261
|
+
fs.removeSync(configPath);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 4. ลบจาก installed list
|
|
266
|
+
this.configManager.removeInstalledPackage(packageName);
|
|
267
|
+
|
|
268
|
+
spinner.succeed(chalk.green(`ถอนการติดตั้ง ${packageName} เสร็จสมบูรณ์!`));
|
|
269
|
+
|
|
270
|
+
return { success: true, package: packageName };
|
|
271
|
+
|
|
272
|
+
} catch (error) {
|
|
273
|
+
spinner.fail(chalk.red(`ถอนการติดตั้งไม่สำเร็จ: ${error.message}`));
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* ลบ skill จาก OpenClaw
|
|
280
|
+
*/
|
|
281
|
+
async removeSkillFromOpenClaw(skillName) {
|
|
282
|
+
const skillPath = path.join(this.configManager.getSkillsPath(), skillName);
|
|
283
|
+
if (fs.existsSync(skillPath)) {
|
|
284
|
+
fs.removeSync(skillPath);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* แสดงข้อมูล dry run
|
|
290
|
+
*/
|
|
291
|
+
showDryRunInfo(pkg, withCron) {
|
|
292
|
+
console.log(chalk.cyan('\n📋 ข้อมูลการติดตั้ง (Dry Run):\n'));
|
|
293
|
+
|
|
294
|
+
console.log(chalk.white('Package:'), chalk.bold(pkg.name));
|
|
295
|
+
console.log(chalk.white('Version:'), pkg.version);
|
|
296
|
+
console.log(chalk.white('Description:'), pkg.description);
|
|
297
|
+
|
|
298
|
+
console.log(chalk.yellow('\n📦 Skills ที่จะติดตั้ง:'));
|
|
299
|
+
pkg.skills.forEach(skill => {
|
|
300
|
+
console.log(` • ${skill.name}@${skill.version}`);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (withCron && pkg.crons && pkg.crons.length > 0) {
|
|
304
|
+
console.log(chalk.yellow('\n⏰ Cronjobs ที่จะตั้ง:'));
|
|
305
|
+
pkg.crons.forEach(cron => {
|
|
306
|
+
console.log(` • ${cron.skill}`);
|
|
307
|
+
console.log(` Schedule: ${cron.schedule}`);
|
|
308
|
+
console.log(` Description: ${cron.description}`);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
console.log(chalk.yellow('\n⚙️ Config ที่จะตั้ง:'));
|
|
313
|
+
if (pkg.config) {
|
|
314
|
+
console.log(JSON.stringify(pkg.config, null, 2));
|
|
315
|
+
} else {
|
|
316
|
+
console.log(' (ไม่มี config พิเศษ)');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.log(chalk.gray('\n(ไม่ได้ทำการติดตั้งจริง - dry run mode)'));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* แสดงสรุปการติดตั้ง
|
|
324
|
+
*/
|
|
325
|
+
showInstallSummary(pkg, crons) {
|
|
326
|
+
console.log(chalk.green('\n✅ สรุปการติดตั้ง:\n'));
|
|
327
|
+
|
|
328
|
+
console.log(chalk.white('📦 Package:'), pkg.name);
|
|
329
|
+
console.log(chalk.white('🛠️ Skills ที่ติดตั้ง:'));
|
|
330
|
+
pkg.skills.forEach(skill => {
|
|
331
|
+
console.log(` ✓ ${skill.name}`);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (crons.length > 0) {
|
|
335
|
+
console.log(chalk.white('\n⏰ Cronjobs ที่ตั้ง:'));
|
|
336
|
+
crons.forEach(cron => {
|
|
337
|
+
console.log(` ✓ ${cron.skill} (${cron.schedule})`);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
console.log(chalk.white('\n📁 Config path:'), this.getPackageConfigPath(pkg.name));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* ดึง path ของ config สำหรับ package
|
|
346
|
+
*/
|
|
347
|
+
getPackageConfigPath(packageName) {
|
|
348
|
+
return path.join(this.configManager.getSkillsPath(), `${packageName}.config.json`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = Installer;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NPMRegistry - Client สำหรับค้นหาและดึง packages จาก npm registry
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
|
|
7
|
+
const NPM_REGISTRY_URL = 'https://registry.npmjs.org';
|
|
8
|
+
const NPM_SEARCH_URL = 'https://api.npms.io/v2';
|
|
9
|
+
|
|
10
|
+
class NPMRegistry {
|
|
11
|
+
constructor(cacheDir = null) {
|
|
12
|
+
this.cache = new Map();
|
|
13
|
+
this.cacheExpiry = 5 * 60 * 1000; // 5 minutes
|
|
14
|
+
this.cacheDir = cacheDir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ค้นหา packages ที่เป็น clawflow packages
|
|
19
|
+
* โดยใช้ keyword 'clawflow' หรือ scope @clawflow
|
|
20
|
+
*/
|
|
21
|
+
async searchClawFlowPackages(query = '') {
|
|
22
|
+
try {
|
|
23
|
+
// ค้นหาด้วย keyword 'clawflow'
|
|
24
|
+
const searchQuery = query ? `${query} clawflow` : 'clawflow';
|
|
25
|
+
const response = await axios.get(`${NPM_SEARCH_URL}/search`, {
|
|
26
|
+
params: {
|
|
27
|
+
q: searchQuery,
|
|
28
|
+
size: 50,
|
|
29
|
+
},
|
|
30
|
+
timeout: 10000,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const results = response.data.results || [];
|
|
34
|
+
return results
|
|
35
|
+
.filter((item) => this.isClawFlowPackage(item.package))
|
|
36
|
+
.map((item) => this.normalizePackage(item.package));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error searching npm packages:', error.message);
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* ตรวจสอบว่าเป็น clawflow package หรือไม่
|
|
45
|
+
*/
|
|
46
|
+
isClawFlowPackage(pkg) {
|
|
47
|
+
if (!pkg) return false;
|
|
48
|
+
|
|
49
|
+
// ตรวจสอบ scope @clawflow/
|
|
50
|
+
if (pkg.name.startsWith('@clawflow/')) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ตรวจสอบ keywords
|
|
55
|
+
const keywords = pkg.keywords || [];
|
|
56
|
+
if (keywords.includes('clawflow')) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ตรวจสอบ field clawflow ใน package.json
|
|
61
|
+
if (pkg.clawflow || (pkg.manifest && pkg.manifest.clawflow)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* ดึงข้อมูล package จาก npm registry
|
|
70
|
+
*/
|
|
71
|
+
async getPackage(name, version = 'latest') {
|
|
72
|
+
const cacheKey = `${name}@${version}`;
|
|
73
|
+
|
|
74
|
+
// ตรวจสอบ cache
|
|
75
|
+
if (this.cache.has(cacheKey)) {
|
|
76
|
+
const cached = this.cache.get(cacheKey);
|
|
77
|
+
if (Date.now() - cached.timestamp < this.cacheExpiry) {
|
|
78
|
+
return cached.data;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const encodedName = this.encodePackageName(name);
|
|
84
|
+
const url =
|
|
85
|
+
version === 'latest'
|
|
86
|
+
? `${NPM_REGISTRY_URL}/${encodedName}/latest`
|
|
87
|
+
: `${NPM_REGISTRY_URL}/${encodedName}/${version}`;
|
|
88
|
+
|
|
89
|
+
const response = await axios.get(url, {
|
|
90
|
+
timeout: 10000,
|
|
91
|
+
headers: {
|
|
92
|
+
Accept: 'application/vnd.npm.install-v1+json',
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const pkg = this.normalizePackage(response.data);
|
|
97
|
+
|
|
98
|
+
// ถ้าเป็น clawflow package ให้ดึง clawflow.json เพิ่ม
|
|
99
|
+
if (this.isClawFlowPackage(response.data)) {
|
|
100
|
+
const clawflowConfig = await this.fetchClawflowConfig(name, response.data.version);
|
|
101
|
+
if (clawflowConfig) {
|
|
102
|
+
Object.assign(pkg, clawflowConfig);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// เก็บ cache
|
|
107
|
+
this.cache.set(cacheKey, {
|
|
108
|
+
data: pkg,
|
|
109
|
+
timestamp: Date.now(),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return pkg;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error.response?.status === 404) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`Failed to fetch package ${name}: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* ดึง clawflow.json จาก package
|
|
123
|
+
*/
|
|
124
|
+
async fetchClawflowConfig(name, version) {
|
|
125
|
+
try {
|
|
126
|
+
// ดึงจาก unpkg หรือ jsDelivr
|
|
127
|
+
const urls = [
|
|
128
|
+
`https://unpkg.com/${name}@${version}/clawflow.json`,
|
|
129
|
+
`https://cdn.jsdelivr.net/npm/${name}@${version}/clawflow.json`,
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
for (const url of urls) {
|
|
133
|
+
try {
|
|
134
|
+
const response = await axios.get(url, { timeout: 5000 });
|
|
135
|
+
return response.data;
|
|
136
|
+
} catch (e) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* ดึงข้อมูล package ทั้งหมด (รวมทุก versions)
|
|
149
|
+
*/
|
|
150
|
+
async getPackageManifest(name) {
|
|
151
|
+
const cacheKey = `manifest:${name}`;
|
|
152
|
+
|
|
153
|
+
if (this.cache.has(cacheKey)) {
|
|
154
|
+
const cached = this.cache.get(cacheKey);
|
|
155
|
+
if (Date.now() - cached.timestamp < this.cacheExpiry) {
|
|
156
|
+
return cached.data;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const encodedName = this.encodePackageName(name);
|
|
162
|
+
const response = await axios.get(`${NPM_REGISTRY_URL}/${encodedName}`, {
|
|
163
|
+
timeout: 10000,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const data = response.data;
|
|
167
|
+
|
|
168
|
+
this.cache.set(cacheKey, {
|
|
169
|
+
data,
|
|
170
|
+
timestamp: Date.now(),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return data;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error.response?.status === 404) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* ดึง latest version ของ package
|
|
184
|
+
*/
|
|
185
|
+
async getLatestVersion(name) {
|
|
186
|
+
const manifest = await this.getPackageManifest(name);
|
|
187
|
+
if (!manifest) return null;
|
|
188
|
+
|
|
189
|
+
return manifest['dist-tags']?.latest || null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* ตรวจสอบว่า package มีอยู่จริงบน npm หรือไม่
|
|
194
|
+
*/
|
|
195
|
+
async exists(name) {
|
|
196
|
+
const pkg = await this.getLatestVersion(name);
|
|
197
|
+
return pkg !== null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* แปลงชื่อ package สำหรับ URL (handle scoped packages)
|
|
202
|
+
*/
|
|
203
|
+
encodePackageName(name) {
|
|
204
|
+
if (name.startsWith('@')) {
|
|
205
|
+
return `@${encodeURIComponent(name.substring(1))}`;
|
|
206
|
+
}
|
|
207
|
+
return encodeURIComponent(name);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Normalize package data จาก npm ให้เป็นรูปแบบ clawflow
|
|
212
|
+
*/
|
|
213
|
+
normalizePackage(pkg) {
|
|
214
|
+
const clawflowField = pkg.clawflow || {};
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
name: pkg.name,
|
|
218
|
+
version: pkg.version,
|
|
219
|
+
description: pkg.description || '',
|
|
220
|
+
author: this.normalizeAuthor(pkg.author),
|
|
221
|
+
keywords: pkg.keywords || [],
|
|
222
|
+
homepage: pkg.homepage || '',
|
|
223
|
+
repository: pkg.repository?.url || pkg.repository || '',
|
|
224
|
+
license: pkg.license || '',
|
|
225
|
+
source: 'npm',
|
|
226
|
+
|
|
227
|
+
// ClawFlow specific fields
|
|
228
|
+
skills: clawflowField.skills || [],
|
|
229
|
+
crons: clawflowField.crons || [],
|
|
230
|
+
config: clawflowField.config || {},
|
|
231
|
+
postInstall: clawflowField.postInstall || '',
|
|
232
|
+
dependencies: pkg.dependencies || {},
|
|
233
|
+
|
|
234
|
+
// Raw npm data
|
|
235
|
+
_npm: {
|
|
236
|
+
dist: pkg.dist,
|
|
237
|
+
maintainers: pkg.maintainers,
|
|
238
|
+
time: pkg.time,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Normalize author field
|
|
245
|
+
*/
|
|
246
|
+
normalizeAuthor(author) {
|
|
247
|
+
if (!author) return 'Unknown';
|
|
248
|
+
if (typeof author === 'string') return author;
|
|
249
|
+
if (typeof author === 'object') {
|
|
250
|
+
return author.name || 'Unknown';
|
|
251
|
+
}
|
|
252
|
+
return 'Unknown';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* ดึงรายการ popular clawflowhub packages
|
|
257
|
+
*/
|
|
258
|
+
async getPopularPackages(limit = 20) {
|
|
259
|
+
try {
|
|
260
|
+
const response = await axios.get(`${NPM_SEARCH_URL}/search`, {
|
|
261
|
+
params: {
|
|
262
|
+
q: 'keywords:clawflowhub',
|
|
263
|
+
sort: 'popularity',
|
|
264
|
+
size: limit,
|
|
265
|
+
},
|
|
266
|
+
timeout: 10000,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const results = response.data.results || [];
|
|
270
|
+
return results.map((item) => this.normalizePackage(item.package));
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error('Error fetching popular packages:', error.message);
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* ล้าง cache
|
|
279
|
+
*/
|
|
280
|
+
clearCache() {
|
|
281
|
+
this.cache.clear();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = NPMRegistry;
|