clawflowbang 1.0.0 → 1.0.2
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/.env.example +14 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +35 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- package/.github/pull_request_template.md +19 -0
- package/.github/workflows/publish.yml +46 -0
- package/.github/workflows/test.yml +31 -0
- package/CODE_OF_CONDUCT.md +29 -0
- package/LICENSE +21 -0
- package/README.md +151 -1
- package/SECURITY.md +20 -0
- package/SUPPORT.md +21 -0
- package/bin/clawflowhub.js +60 -64
- package/package.json +18 -16
- package/src/commands/cron.js +71 -36
- package/src/commands/init.js +23 -23
- package/src/commands/install.js +17 -17
- package/src/commands/list.js +27 -25
- package/src/commands/remove.js +4 -4
- package/src/commands/search.js +23 -20
- package/src/commands/status.js +19 -25
- package/src/core/ConfigManager.js +132 -132
- package/src/core/CronManager.js +245 -245
- package/src/core/Installer.js +53 -53
- package/src/core/OpenClawCLI.js +62 -17
- package/src/core/TerminalUI.js +332 -0
- package/.eslintrc.json +0 -38
- package/__tests__/config-manager.test.js +0 -52
- package/__tests__/cron-format.test.js +0 -26
- package/__tests__/cron-manager.local.test.js +0 -65
- package/__tests__/openclaw-cli.test.js +0 -51
- package/docs/clawhub-package-format.md +0 -179
- package/examples/npm-package-example/package.json +0 -53
package/src/core/Installer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Installer -
|
|
2
|
+
* Installer - Manage installation and uninstallation of packages
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const fs = require('fs-extra');
|
|
@@ -13,7 +13,7 @@ class Installer {
|
|
|
13
13
|
constructor(configManager, registry) {
|
|
14
14
|
this.configManager = configManager;
|
|
15
15
|
this.registry = registry;
|
|
16
|
-
this.cronManager = null; //
|
|
16
|
+
this.cronManager = null; // set from outside
|
|
17
17
|
this.openclawCLI = new OpenClawCLI(configManager);
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -22,25 +22,25 @@ class Installer {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
|
|
25
|
+
* Install package
|
|
26
26
|
*/
|
|
27
27
|
async install(packageName, options = {}) {
|
|
28
28
|
const { global = false, cron: withCron = true, dryRun = false } = options;
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// Check whether the package exists (supports built-in and npm)
|
|
31
31
|
const pkg = await this.registry.getPackage(packageName);
|
|
32
32
|
if (!pkg) {
|
|
33
|
-
throw new Error(`Package "${packageName}"
|
|
33
|
+
throw new Error(`Package "${packageName}" not found in registry (check package name or internet connection)`);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// ตรวจสอบว่าติดตั้งแล้วหรือยัง
|
|
37
37
|
const installed = this.configManager.getInstalledPackages();
|
|
38
38
|
if (installed[packageName]) {
|
|
39
|
-
console.log(chalk.yellow(`⚠️ Package "${packageName}"
|
|
39
|
+
console.log(chalk.yellow(`⚠️ Package "${packageName}" is already installed`));
|
|
40
40
|
const { reinstall } = await inquirer.prompt([{
|
|
41
41
|
type: 'confirm',
|
|
42
42
|
name: 'reinstall',
|
|
43
|
-
message: '
|
|
43
|
+
message: 'Do you want to reinstall?',
|
|
44
44
|
default: false,
|
|
45
45
|
}]);
|
|
46
46
|
if (!reinstall) {
|
|
@@ -48,7 +48,7 @@ class Installer {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
console.log(chalk.cyan(`📦
|
|
51
|
+
console.log(chalk.cyan(`📦 Installing ${chalk.bold(packageName)}...\n`));
|
|
52
52
|
|
|
53
53
|
// Dry run - แสดงเฉพาะข้อมูล
|
|
54
54
|
if (dryRun) {
|
|
@@ -56,26 +56,26 @@ class Installer {
|
|
|
56
56
|
return { success: true, dryRun: true };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const spinner = ora('
|
|
59
|
+
const spinner = ora('Processing...').start();
|
|
60
60
|
|
|
61
61
|
try {
|
|
62
62
|
// 1. ติดตั้ง skills
|
|
63
|
-
spinner.text = '
|
|
63
|
+
spinner.text = 'Installing skills...';
|
|
64
64
|
await this.installSkills(pkg.skills, global);
|
|
65
65
|
|
|
66
66
|
// 2. ตั้งค่า config
|
|
67
|
-
spinner.text = '
|
|
67
|
+
spinner.text = 'Setting up configuration...';
|
|
68
68
|
await this.setupConfig(packageName, pkg.config);
|
|
69
69
|
|
|
70
70
|
// 3. ตั้ง cronjobs (ถ้าเปิดใช้งาน)
|
|
71
71
|
let crons = [];
|
|
72
72
|
if (withCron && pkg.crons && pkg.crons.length > 0) {
|
|
73
|
-
spinner.text = '
|
|
73
|
+
spinner.text = 'Setting up cronjobs...';
|
|
74
74
|
crons = await this.setupCrons(pkg.crons);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// 4. บันทึกว่าติดตั้งแล้ว
|
|
78
|
-
spinner.text = '
|
|
78
|
+
spinner.text = 'Saving state...';
|
|
79
79
|
this.configManager.addInstalledPackage(packageName, {
|
|
80
80
|
version: pkg.version,
|
|
81
81
|
skills: pkg.skills.map(s => s.name),
|
|
@@ -83,14 +83,14 @@ class Installer {
|
|
|
83
83
|
configPath: this.getPackageConfigPath(packageName),
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
spinner.succeed(chalk.green(
|
|
86
|
+
spinner.succeed(chalk.green(`Installed ${packageName} successfully!`));
|
|
87
87
|
|
|
88
88
|
// แสดงข้อมูลสรุป
|
|
89
89
|
this.showInstallSummary(pkg, crons);
|
|
90
90
|
|
|
91
91
|
// แสดง post-install message
|
|
92
92
|
if (pkg.postInstall) {
|
|
93
|
-
console.log(chalk.yellow('\n📋
|
|
93
|
+
console.log(chalk.yellow('\n📋 Next steps:'));
|
|
94
94
|
console.log(chalk.gray(` ${pkg.postInstall}`));
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -102,20 +102,20 @@ class Installer {
|
|
|
102
102
|
};
|
|
103
103
|
|
|
104
104
|
} catch (error) {
|
|
105
|
-
spinner.fail(chalk.red(
|
|
105
|
+
spinner.fail(chalk.red(`Installation failed: ${error.message}`));
|
|
106
106
|
throw error;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
|
|
111
|
+
* Install skills
|
|
112
112
|
*/
|
|
113
113
|
async installSkills(skills, global = false) {
|
|
114
114
|
for (const skill of skills) {
|
|
115
|
-
console.log(chalk.gray(` →
|
|
115
|
+
console.log(chalk.gray(` → Installing skill: ${skill.name}@${skill.version}`));
|
|
116
116
|
|
|
117
|
-
//
|
|
118
|
-
//
|
|
117
|
+
// Install skill via OpenClaw
|
|
118
|
+
// In production this would call OpenClaw API or CLI
|
|
119
119
|
await this.installSkillToOpenClaw(skill, global);
|
|
120
120
|
}
|
|
121
121
|
}
|
|
@@ -123,33 +123,33 @@ class Installer {
|
|
|
123
123
|
/**
|
|
124
124
|
* ติดตั้ง skill ไปยัง OpenClaw
|
|
125
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
|
|
140
|
-
return;
|
|
141
|
-
} catch (gitError) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
|
|
144
|
-
`clawhub: ${error.message}\n` +
|
|
145
|
-
`git: ${gitError.message}`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
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 succeeded: ${cloned.repository}`));
|
|
140
|
+
return;
|
|
141
|
+
} catch (gitError) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Failed to install skill "${skill.name}" from both clawhub and git clone\n` +
|
|
144
|
+
`clawhub: ${error.message}\n` +
|
|
145
|
+
`git: ${gitError.message}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
|
|
152
|
+
* Setup config for package
|
|
153
153
|
*/
|
|
154
154
|
async setupConfig(packageName, configSchema) {
|
|
155
155
|
if (!configSchema) return;
|
|
@@ -166,35 +166,35 @@ class Installer {
|
|
|
166
166
|
|
|
167
167
|
for (const [key, value] of Object.entries(schema)) {
|
|
168
168
|
if (value.env) {
|
|
169
|
-
//
|
|
169
|
+
// Pull value from environment variable
|
|
170
170
|
const envValue = process.env[value.env];
|
|
171
171
|
if (envValue) {
|
|
172
172
|
newConfig[skillName][key] = envValue;
|
|
173
173
|
} else if (value.required && !existingConfig[skillName]?.[key]) {
|
|
174
|
-
//
|
|
174
|
+
// If not in env and required
|
|
175
175
|
const answer = await inquirer.prompt([{
|
|
176
176
|
type: 'input',
|
|
177
177
|
name: key,
|
|
178
|
-
message:
|
|
179
|
-
validate: (input) => input.length > 0 || '
|
|
178
|
+
message: `Please enter ${key} for ${skillName} (or set ${value.env}):`,
|
|
179
|
+
validate: (input) => input.length > 0 || 'This field is required',
|
|
180
180
|
}]);
|
|
181
181
|
newConfig[skillName][key] = answer[key];
|
|
182
182
|
}
|
|
183
183
|
} else if (value.default !== undefined) {
|
|
184
184
|
newConfig[skillName][key] = existingConfig[skillName]?.[key] ?? value.default;
|
|
185
185
|
} else if (value.required && !existingConfig[skillName]?.[key]) {
|
|
186
|
-
|
|
186
|
+
const answer = await inquirer.prompt([{
|
|
187
187
|
type: 'input',
|
|
188
188
|
name: key,
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
message: `Please enter ${key} for ${skillName}:`,
|
|
190
|
+
validate: (input) => input.length > 0 || 'This field is required',
|
|
191
191
|
}]);
|
|
192
192
|
newConfig[skillName][key] = answer[key];
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
//
|
|
197
|
+
// Merge with existing config
|
|
198
198
|
const mergedConfig = { ...existingConfig, ...newConfig };
|
|
199
199
|
fs.writeJsonSync(configPath, mergedConfig, { spaces: 2 });
|
|
200
200
|
}
|
package/src/core/OpenClawCLI.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
3
|
const { promisify } = require('util');
|
|
4
|
-
const { execFile, spawnSync } = require('child_process');
|
|
4
|
+
const { execFile, spawn, spawnSync } = require('child_process');
|
|
5
5
|
|
|
6
6
|
const execFileAsync = promisify(execFile);
|
|
7
7
|
|
|
@@ -33,22 +33,67 @@ class OpenClawCLI {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
commandExists(command, args = ['--help']) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
try {
|
|
37
|
+
const result = spawnSync(command, args, {
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
stdio: 'pipe',
|
|
40
|
+
windowsHide: true,
|
|
41
|
+
shell: true,
|
|
42
|
+
});
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
return result.status === 0;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
async run(command, args, options = {}) {
|
|
51
|
+
if (process.platform === 'win32') {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const comspec = process.env.ComSpec || 'cmd.exe';
|
|
54
|
+
|
|
55
|
+
const child = spawn(comspec, ['/d', '/s', '/c', command, ...args], {
|
|
56
|
+
cwd: options.cwd,
|
|
57
|
+
env: process.env,
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
shell: false,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
let stdout = '';
|
|
63
|
+
let stderr = '';
|
|
64
|
+
|
|
65
|
+
child.stdout.on('data', (chunk) => {
|
|
66
|
+
stdout += String(chunk);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
child.stderr.on('data', (chunk) => {
|
|
70
|
+
stderr += String(chunk);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
child.on('error', (error) => {
|
|
74
|
+
reject(new Error(`${command} ${args.join(' ')} ล้มเหลว: ${error.message}`));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
child.on('close', (code) => {
|
|
78
|
+
const out = stdout.trim();
|
|
79
|
+
const err = stderr.trim();
|
|
80
|
+
if (code === 0) {
|
|
81
|
+
resolve({ stdout: out, stderr: err });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const detail = err || out || `exit code ${code}`;
|
|
85
|
+
reject(new Error(`${command} ${args.join(' ')} ล้มเหลว: ${detail}`));
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
46
90
|
try {
|
|
47
91
|
const { stdout, stderr } = await execFileAsync(command, args, {
|
|
48
92
|
cwd: options.cwd,
|
|
49
93
|
env: process.env,
|
|
50
94
|
maxBuffer: 1024 * 1024 * 4,
|
|
51
95
|
windowsHide: true,
|
|
96
|
+
shell: true,
|
|
52
97
|
});
|
|
53
98
|
return {
|
|
54
99
|
stdout: (stdout || '').trim(),
|
|
@@ -58,7 +103,7 @@ class OpenClawCLI {
|
|
|
58
103
|
const stderr = error.stderr ? String(error.stderr).trim() : '';
|
|
59
104
|
const stdout = error.stdout ? String(error.stdout).trim() : '';
|
|
60
105
|
const detail = stderr || stdout || error.message;
|
|
61
|
-
throw new Error(`${command} ${args.join(' ')}
|
|
106
|
+
throw new Error(`${command} ${args.join(' ')} failed: ${detail}`);
|
|
62
107
|
}
|
|
63
108
|
}
|
|
64
109
|
|
|
@@ -73,7 +118,7 @@ class OpenClawCLI {
|
|
|
73
118
|
|
|
74
119
|
async installSkill(skillName, skillVersion, skillsPath) {
|
|
75
120
|
if (!this.hasClawhub()) {
|
|
76
|
-
throw new Error('
|
|
121
|
+
throw new Error('CLI "clawhub" not found (required to install skills from registry)');
|
|
77
122
|
}
|
|
78
123
|
|
|
79
124
|
const workdir = path.dirname(skillsPath);
|
|
@@ -140,17 +185,17 @@ class OpenClawCLI {
|
|
|
140
185
|
|
|
141
186
|
async installSkillFromGit(skill, skillsPath) {
|
|
142
187
|
if (!this.hasGit()) {
|
|
143
|
-
throw new Error('
|
|
188
|
+
throw new Error('git command not found (required for fallback install)');
|
|
144
189
|
}
|
|
145
190
|
|
|
146
191
|
const repoUrl = this.resolveGitRepository(skill);
|
|
147
192
|
if (!repoUrl) {
|
|
148
|
-
throw new Error('
|
|
193
|
+
throw new Error('No git repository information available for fallback install');
|
|
149
194
|
}
|
|
150
195
|
|
|
151
196
|
const skillDirName = this.resolveSkillDirName(skill);
|
|
152
197
|
if (!skillDirName) {
|
|
153
|
-
throw new Error('
|
|
198
|
+
throw new Error('Unable to determine skill directory name from provided data');
|
|
154
199
|
}
|
|
155
200
|
|
|
156
201
|
const targetDir = path.join(skillsPath, skillDirName);
|
|
@@ -170,7 +215,7 @@ class OpenClawCLI {
|
|
|
170
215
|
|
|
171
216
|
const skillFile = path.join(targetDir, 'SKILL.md');
|
|
172
217
|
if (!fs.existsSync(skillFile)) {
|
|
173
|
-
throw new Error(`
|
|
218
|
+
throw new Error(`Repository "${repoUrl}" does not contain SKILL.md at the repository root`);
|
|
174
219
|
}
|
|
175
220
|
|
|
176
221
|
return {
|
|
@@ -192,7 +237,7 @@ class OpenClawCLI {
|
|
|
192
237
|
|
|
193
238
|
async addCronJob({ name, description, schedule, message }) {
|
|
194
239
|
if (!this.hasOpenClaw()) {
|
|
195
|
-
throw new Error('
|
|
240
|
+
throw new Error('openclaw CLI not found (required to create native cronjobs)');
|
|
196
241
|
}
|
|
197
242
|
|
|
198
243
|
const args = [
|
|
@@ -222,7 +267,7 @@ class OpenClawCLI {
|
|
|
222
267
|
|
|
223
268
|
async listCronJobs() {
|
|
224
269
|
if (!this.hasOpenClaw()) {
|
|
225
|
-
throw new Error('
|
|
270
|
+
throw new Error('openclaw CLI not found (required to list cronjobs)');
|
|
226
271
|
}
|
|
227
272
|
|
|
228
273
|
const result = await this.run(this.getOpenClawBin(), ['cron', 'list', '--all', '--json']);
|
|
@@ -232,7 +277,7 @@ class OpenClawCLI {
|
|
|
232
277
|
|
|
233
278
|
async removeCronJob(jobId) {
|
|
234
279
|
if (!this.hasOpenClaw()) {
|
|
235
|
-
throw new Error('
|
|
280
|
+
throw new Error('openclaw CLI not found (required to remove cronjobs)');
|
|
236
281
|
}
|
|
237
282
|
|
|
238
283
|
await this.run(this.getOpenClawBin(), ['cron', 'rm', jobId, '--json']);
|
|
@@ -241,7 +286,7 @@ class OpenClawCLI {
|
|
|
241
286
|
|
|
242
287
|
async editCronJob(jobId, updates = {}) {
|
|
243
288
|
if (!this.hasOpenClaw()) {
|
|
244
|
-
throw new Error('
|
|
289
|
+
throw new Error('openclaw CLI not found (required to edit cronjobs)');
|
|
245
290
|
}
|
|
246
291
|
|
|
247
292
|
const args = ['cron', 'edit', jobId];
|