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.
@@ -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;