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,265 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const { promisify } = require('util');
|
|
4
|
+
const { execFile, spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
class OpenClawCLI {
|
|
9
|
+
constructor(configManager) {
|
|
10
|
+
this.configManager = configManager;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getOpenClawBin() {
|
|
14
|
+
const config = this.configManager.getConfig();
|
|
15
|
+
return config.openclaw?.cliBin || 'openclaw';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getClawhubBin() {
|
|
19
|
+
const config = this.configManager.getConfig();
|
|
20
|
+
return config.openclaw?.clawhubBin || 'clawhub';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
hasOpenClaw() {
|
|
24
|
+
return this.commandExists(this.getOpenClawBin(), ['--version']);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
hasClawhub() {
|
|
28
|
+
return this.commandExists(this.getClawhubBin(), ['--version']);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
hasGit() {
|
|
32
|
+
return this.commandExists('git', ['--version']);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
commandExists(command, args = ['--help']) {
|
|
36
|
+
const result = spawnSync(command, args, {
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
stdio: 'pipe',
|
|
39
|
+
windowsHide: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return result.status === 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async run(command, args, options = {}) {
|
|
46
|
+
try {
|
|
47
|
+
const { stdout, stderr } = await execFileAsync(command, args, {
|
|
48
|
+
cwd: options.cwd,
|
|
49
|
+
env: process.env,
|
|
50
|
+
maxBuffer: 1024 * 1024 * 4,
|
|
51
|
+
windowsHide: true,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
stdout: (stdout || '').trim(),
|
|
55
|
+
stderr: (stderr || '').trim(),
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const stderr = error.stderr ? String(error.stderr).trim() : '';
|
|
59
|
+
const stdout = error.stdout ? String(error.stdout).trim() : '';
|
|
60
|
+
const detail = stderr || stdout || error.message;
|
|
61
|
+
throw new Error(`${command} ${args.join(' ')} ล้มเหลว: ${detail}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
parseJson(text) {
|
|
66
|
+
if (!text) return null;
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(text);
|
|
69
|
+
} catch (_error) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async installSkill(skillName, skillVersion, skillsPath) {
|
|
75
|
+
if (!this.hasClawhub()) {
|
|
76
|
+
throw new Error('ไม่พบ CLI "clawhub" (ต้องใช้สำหรับติดตั้ง skill จาก registry)');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const workdir = path.dirname(skillsPath);
|
|
80
|
+
const dir = path.basename(skillsPath);
|
|
81
|
+
const args = ['install', skillName, '--workdir', workdir, '--dir', dir, '--force', '--no-input'];
|
|
82
|
+
|
|
83
|
+
if (skillVersion && skillVersion !== 'latest') {
|
|
84
|
+
args.push('--version', skillVersion);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await this.run(this.getClawhubBin(), args);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
resolveGitRepository(skill = {}) {
|
|
92
|
+
const fromFields = [
|
|
93
|
+
skill.git,
|
|
94
|
+
skill.repo,
|
|
95
|
+
skill.url,
|
|
96
|
+
typeof skill.repository === 'string' ? skill.repository : skill.repository?.url,
|
|
97
|
+
].find((value) => typeof value === 'string' && value.trim());
|
|
98
|
+
|
|
99
|
+
if (fromFields) {
|
|
100
|
+
return fromFields.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (skill.source === 'github' && typeof skill.name === 'string' && skill.name.includes('/')) {
|
|
104
|
+
return `https://github.com/${skill.name}.git`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
(skill.source === 'git' || skill.source === 'github') &&
|
|
109
|
+
typeof skill.name === 'string' &&
|
|
110
|
+
/^(https?:\/\/|git@|ssh:\/\/)/i.test(skill.name)
|
|
111
|
+
) {
|
|
112
|
+
return skill.name;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
resolveSkillDirName(skill = {}) {
|
|
119
|
+
if (typeof skill.dir === 'string' && skill.dir.trim()) {
|
|
120
|
+
return skill.dir.trim();
|
|
121
|
+
}
|
|
122
|
+
if (typeof skill.localName === 'string' && skill.localName.trim()) {
|
|
123
|
+
return skill.localName.trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let name = (skill.name || '').trim();
|
|
127
|
+
if (!name) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (/^(https?:\/\/|git@|ssh:\/\/)/i.test(name)) {
|
|
132
|
+
name = name.split('/').pop() || name;
|
|
133
|
+
name = name.replace(/\.git$/i, '');
|
|
134
|
+
} else if (name.includes('/')) {
|
|
135
|
+
name = name.split('/').pop();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return name || null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async installSkillFromGit(skill, skillsPath) {
|
|
142
|
+
if (!this.hasGit()) {
|
|
143
|
+
throw new Error('ไม่พบคำสั่ง "git" สำหรับ fallback install');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const repoUrl = this.resolveGitRepository(skill);
|
|
147
|
+
if (!repoUrl) {
|
|
148
|
+
throw new Error('ไม่มีข้อมูล git repository สำหรับ fallback install');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const skillDirName = this.resolveSkillDirName(skill);
|
|
152
|
+
if (!skillDirName) {
|
|
153
|
+
throw new Error('ไม่สามารถระบุชื่อโฟลเดอร์ skill จากข้อมูลที่มี');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const targetDir = path.join(skillsPath, skillDirName);
|
|
157
|
+
fs.ensureDirSync(skillsPath);
|
|
158
|
+
if (fs.existsSync(targetDir)) {
|
|
159
|
+
fs.removeSync(targetDir);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const args = ['clone', '--depth', '1'];
|
|
163
|
+
const gitRef = skill.gitRef || skill.ref || skill.branch || skill.tag;
|
|
164
|
+
if (typeof gitRef === 'string' && gitRef.trim()) {
|
|
165
|
+
args.push('--branch', gitRef.trim());
|
|
166
|
+
}
|
|
167
|
+
args.push(repoUrl, targetDir);
|
|
168
|
+
|
|
169
|
+
await this.run('git', args);
|
|
170
|
+
|
|
171
|
+
const skillFile = path.join(targetDir, 'SKILL.md');
|
|
172
|
+
if (!fs.existsSync(skillFile)) {
|
|
173
|
+
throw new Error(`repo "${repoUrl}" ไม่มีไฟล์ SKILL.md ที่ root`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
name: skill.name,
|
|
178
|
+
dir: skillDirName,
|
|
179
|
+
repository: repoUrl,
|
|
180
|
+
path: targetDir,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async verifySkill(skillName) {
|
|
185
|
+
if (!this.hasOpenClaw()) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await this.run(this.getOpenClawBin(), ['skills', 'info', skillName, '--json']);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async addCronJob({ name, description, schedule, message }) {
|
|
194
|
+
if (!this.hasOpenClaw()) {
|
|
195
|
+
throw new Error('ไม่พบ CLI "openclaw" (ต้องใช้สำหรับสร้าง cronjob แบบ native)');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const args = [
|
|
199
|
+
'cron',
|
|
200
|
+
'add',
|
|
201
|
+
'--name',
|
|
202
|
+
name,
|
|
203
|
+
'--cron',
|
|
204
|
+
schedule,
|
|
205
|
+
'--session',
|
|
206
|
+
'isolated',
|
|
207
|
+
'--message',
|
|
208
|
+
message,
|
|
209
|
+
'--json',
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
if (description) {
|
|
213
|
+
args.push('--description', description);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const result = await this.run(this.getOpenClawBin(), args);
|
|
217
|
+
const payload = this.parseJson(result.stdout);
|
|
218
|
+
const jobId = payload?.job?.id || payload?.id || null;
|
|
219
|
+
|
|
220
|
+
return { jobId, raw: payload };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async listCronJobs() {
|
|
224
|
+
if (!this.hasOpenClaw()) {
|
|
225
|
+
throw new Error('ไม่พบ CLI "openclaw" (ต้องใช้สำหรับอ่าน cronjobs)');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const result = await this.run(this.getOpenClawBin(), ['cron', 'list', '--all', '--json']);
|
|
229
|
+
const payload = this.parseJson(result.stdout);
|
|
230
|
+
return payload?.jobs || [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async removeCronJob(jobId) {
|
|
234
|
+
if (!this.hasOpenClaw()) {
|
|
235
|
+
throw new Error('ไม่พบ CLI "openclaw" (ต้องใช้สำหรับลบ cronjob)');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await this.run(this.getOpenClawBin(), ['cron', 'rm', jobId, '--json']);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async editCronJob(jobId, updates = {}) {
|
|
243
|
+
if (!this.hasOpenClaw()) {
|
|
244
|
+
throw new Error('ไม่พบ CLI "openclaw" (ต้องใช้สำหรับแก้ไข cronjob)');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const args = ['cron', 'edit', jobId];
|
|
248
|
+
|
|
249
|
+
if (updates.schedule) {
|
|
250
|
+
args.push('--cron', updates.schedule);
|
|
251
|
+
}
|
|
252
|
+
if (typeof updates.description === 'string') {
|
|
253
|
+
args.push('--description', updates.description);
|
|
254
|
+
}
|
|
255
|
+
if (updates.message) {
|
|
256
|
+
args.push('--message', updates.message);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
args.push('--json');
|
|
260
|
+
const result = await this.run(this.getOpenClawBin(), args);
|
|
261
|
+
return this.parseJson(result.stdout);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = OpenClawCLI;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PackageResolver - จัดการ dependency resolution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
class PackageResolver {
|
|
6
|
+
constructor(registry) {
|
|
7
|
+
this.registry = registry;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* แก้ไข dependencies ของ package
|
|
12
|
+
*/
|
|
13
|
+
resolveDependencies(packageName, resolved = new Set(), unresolved = new Set()) {
|
|
14
|
+
// เพิ่มเข้า unresolved
|
|
15
|
+
unresolved.add(packageName);
|
|
16
|
+
|
|
17
|
+
const pkg = this.registry.getPackage(packageName);
|
|
18
|
+
if (!pkg) {
|
|
19
|
+
throw new Error(`Package "${packageName}" ไม่พบใน registry`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ดึง dependencies
|
|
23
|
+
const dependencies = this.registry.getDependencies(packageName);
|
|
24
|
+
|
|
25
|
+
for (const dep of dependencies) {
|
|
26
|
+
if (resolved.has(dep)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (unresolved.has(dep)) {
|
|
31
|
+
throw new Error(`พบ circular dependency: ${packageName} -> ${dep}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.resolveDependencies(dep, resolved, unresolved);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ย้ายจาก unresolved ไป resolved
|
|
38
|
+
unresolved.delete(packageName);
|
|
39
|
+
resolved.add(packageName);
|
|
40
|
+
|
|
41
|
+
return Array.from(resolved);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* ได้รับลำดับการติดตั้งที่ถูกต้อง
|
|
46
|
+
*/
|
|
47
|
+
getInstallOrder(packageNames) {
|
|
48
|
+
const resolved = new Set();
|
|
49
|
+
const unresolved = new Set();
|
|
50
|
+
|
|
51
|
+
for (const name of packageNames) {
|
|
52
|
+
if (!resolved.has(name)) {
|
|
53
|
+
this.resolveDependencies(name, resolved, unresolved);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Array.from(resolved);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* ตรวจสอบ conflicts
|
|
62
|
+
*/
|
|
63
|
+
checkConflicts(packages) {
|
|
64
|
+
const conflicts = [];
|
|
65
|
+
const skillVersions = new Map();
|
|
66
|
+
|
|
67
|
+
for (const pkgName of packages) {
|
|
68
|
+
const pkg = this.registry.getPackage(pkgName);
|
|
69
|
+
if (!pkg) continue;
|
|
70
|
+
|
|
71
|
+
for (const skill of pkg.skills) {
|
|
72
|
+
if (skillVersions.has(skill.name)) {
|
|
73
|
+
const existing = skillVersions.get(skill.name);
|
|
74
|
+
if (existing.version !== skill.version) {
|
|
75
|
+
conflicts.push({
|
|
76
|
+
skill: skill.name,
|
|
77
|
+
packages: [existing.package, pkgName],
|
|
78
|
+
versions: [existing.version, skill.version],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
skillVersions.set(skill.name, {
|
|
83
|
+
package: pkgName,
|
|
84
|
+
version: skill.version,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return conflicts;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = PackageResolver;
|