@skunkceo/cli 1.0.0 → 1.0.1

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/bin/setup.js ADDED
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env node
2
+
3
+ const https = require('https');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { execSync, spawn } = require('child_process');
7
+ const readline = require('readline');
8
+
9
+ // ═══════════════════════════════════════════════════════════════════════════
10
+ // ASCII Art
11
+ // ═══════════════════════════════════════════════════════════════════════════
12
+
13
+ const SKUNK_LOGO = `
14
+ ███████╗██╗ ██╗██╗ ██╗███╗ ██╗██╗ ██╗
15
+ ██╔════╝██║ ██╔╝██║ ██║████╗ ██║██║ ██╔╝
16
+ ███████╗█████╔╝ ██║ ██║██╔██╗ ██║█████╔╝
17
+ ╚════██║██╔═██╗ ██║ ██║██║╚██╗██║██╔═██╗
18
+ ███████║██║ ██╗╚██████╔╝██║ ╚████║██║ ██╗
19
+ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝
20
+ GLOBAL
21
+ `;
22
+
23
+ // ═══════════════════════════════════════════════════════════════════════════
24
+ // Utilities
25
+ // ═══════════════════════════════════════════════════════════════════════════
26
+
27
+ const colors = {
28
+ reset: '\x1b[0m',
29
+ bright: '\x1b[1m',
30
+ dim: '\x1b[2m',
31
+ green: '\x1b[32m',
32
+ yellow: '\x1b[33m',
33
+ red: '\x1b[31m',
34
+ cyan: '\x1b[36m',
35
+ magenta: '\x1b[35m',
36
+ white: '\x1b[37m',
37
+ };
38
+
39
+ function log(msg = '') {
40
+ console.log(msg);
41
+ }
42
+
43
+ function success(msg) {
44
+ console.log(`${colors.green}✓${colors.reset} ${msg}`);
45
+ }
46
+
47
+ function warn(msg) {
48
+ console.log(`${colors.yellow}!${colors.reset} ${msg}`);
49
+ }
50
+
51
+ function error(msg) {
52
+ console.log(`${colors.red}✗${colors.reset} ${msg}`);
53
+ }
54
+
55
+ function step(num, total, msg) {
56
+ console.log(`\n${colors.cyan}[${num}/${total}]${colors.reset} ${colors.bright}${msg}${colors.reset}`);
57
+ }
58
+
59
+ function commandExists(cmd) {
60
+ try {
61
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ function getVersion(cmd) {
69
+ try {
70
+ return execSync(`${cmd} --version`, { encoding: 'utf8' }).trim().split('\n')[0];
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ // ═══════════════════════════════════════════════════════════════════════════
77
+ // Prompts
78
+ // ═══════════════════════════════════════════════════════════════════════════
79
+
80
+ const rl = readline.createInterface({
81
+ input: process.stdin,
82
+ output: process.stdout,
83
+ });
84
+
85
+ function ask(question) {
86
+ return new Promise((resolve) => {
87
+ rl.question(question, resolve);
88
+ });
89
+ }
90
+
91
+ async function confirm(question, defaultYes = true) {
92
+ const hint = defaultYes ? '(Y/n)' : '(y/N)';
93
+ const answer = await ask(`${question} ${hint} `);
94
+ if (!answer) return defaultYes;
95
+ return answer.toLowerCase().startsWith('y');
96
+ }
97
+
98
+ async function choice(question, options) {
99
+ log(question);
100
+ options.forEach((opt, i) => {
101
+ log(` ${colors.cyan}${i + 1}${colors.reset}) ${opt.label}`);
102
+ });
103
+ const answer = await ask(`\nChoice (1-${options.length}): `);
104
+ const idx = parseInt(answer, 10) - 1;
105
+ if (idx >= 0 && idx < options.length) {
106
+ return options[idx].value;
107
+ }
108
+ return options[0].value; // default to first
109
+ }
110
+
111
+ // ═══════════════════════════════════════════════════════════════════════════
112
+ // License Validation
113
+ // ═══════════════════════════════════════════════════════════════════════════
114
+
115
+ async function validateLicense(licenseKey, product) {
116
+ return new Promise((resolve) => {
117
+ const postData = JSON.stringify({
118
+ license_key: licenseKey.toUpperCase().trim(),
119
+ site_url: 'cli-setup', // Validation only, not activation
120
+ });
121
+
122
+ const options = {
123
+ hostname: 'skunkglobal.com',
124
+ port: 443,
125
+ path: '/api/license/validate',
126
+ method: 'POST',
127
+ headers: {
128
+ 'Content-Type': 'application/json',
129
+ 'User-Agent': 'skunk-cli',
130
+ },
131
+ };
132
+
133
+ const req = https.request(options, (res) => {
134
+ let data = '';
135
+ res.on('data', (chunk) => (data += chunk));
136
+ res.on('end', () => {
137
+ try {
138
+ const json = JSON.parse(data);
139
+ if (json.success && json.data && json.data.valid) {
140
+ resolve({
141
+ valid: true,
142
+ activationsLeft: json.data.max_sites ? json.data.max_sites - (json.data.active_sites || 0) : 'unlimited',
143
+ productsCovered: json.data.products_covered || [product],
144
+ });
145
+ } else {
146
+ resolve({ valid: false, error: json.message || 'Invalid license' });
147
+ }
148
+ } catch (e) {
149
+ resolve({ valid: false, error: 'Failed to validate license' });
150
+ }
151
+ });
152
+ });
153
+
154
+ req.on('error', () => {
155
+ resolve({ valid: false, error: 'Could not connect to license server' });
156
+ });
157
+
158
+ req.write(postData);
159
+ req.end();
160
+ });
161
+ }
162
+
163
+ // ═══════════════════════════════════════════════════════════════════════════
164
+ // Installation Functions
165
+ // ═══════════════════════════════════════════════════════════════════════════
166
+
167
+ const PRODUCTS = [
168
+ {
169
+ name: 'SkunkCRM',
170
+ slug: 'skunkcrm',
171
+ skill: 'skunkcrm',
172
+ freeRepo: 'skunkceo/skunkcrm',
173
+ proSlug: 'skunkcrm-pro',
174
+ },
175
+ {
176
+ name: 'SkunkForms',
177
+ slug: 'skunkforms',
178
+ skill: 'skunkforms',
179
+ freeRepo: 'skunkceo/skunkforms',
180
+ proSlug: 'skunkforms-pro',
181
+ },
182
+ {
183
+ name: 'SkunkPages',
184
+ slug: 'skunkpages',
185
+ skill: 'skunkpages',
186
+ freeRepo: 'skunkceo/skunkpages',
187
+ proSlug: 'skunkpages-pro',
188
+ },
189
+ ];
190
+
191
+ async function installPlugin(repo, pluginSlug, wpPath = null) {
192
+ const url = `https://github.com/${repo}/releases/latest/download/${pluginSlug}.zip`;
193
+
194
+ // If WP path provided and wp-cli available, install directly
195
+ if (wpPath && commandExists('wp')) {
196
+ try {
197
+ log(` Installing ${pluginSlug} to WordPress...`);
198
+ execSync(`wp plugin install "${url}" --path="${wpPath}"`, { stdio: 'pipe' });
199
+ return { status: 'installed', location: 'wordpress' };
200
+ } catch (e) {
201
+ // Fall through to cache
202
+ }
203
+ }
204
+
205
+ // Otherwise, cache in ~/.skunk/plugins/ for later
206
+ const cacheDir = path.join(process.env.HOME, '.skunk', 'plugins');
207
+ fs.mkdirSync(cacheDir, { recursive: true });
208
+
209
+ const zipPath = path.join(cacheDir, `${pluginSlug}.zip`);
210
+
211
+ log(` Downloading ${pluginSlug}...`);
212
+
213
+ return new Promise((resolve) => {
214
+ const file = fs.createWriteStream(zipPath);
215
+
216
+ https.get(url, { headers: { 'User-Agent': 'skunk-cli' } }, (res) => {
217
+ // Handle GitHub redirect
218
+ if (res.statusCode === 302 || res.statusCode === 301) {
219
+ https.get(res.headers.location, { headers: { 'User-Agent': 'skunk-cli' } }, (res2) => {
220
+ res2.pipe(file);
221
+ file.on('finish', () => {
222
+ file.close();
223
+ resolve({ status: 'cached', location: zipPath });
224
+ });
225
+ }).on('error', () => resolve({ status: 'failed' }));
226
+ return;
227
+ }
228
+
229
+ if (res.statusCode !== 200) {
230
+ resolve({ status: 'failed', error: `HTTP ${res.statusCode}` });
231
+ return;
232
+ }
233
+
234
+ res.pipe(file);
235
+ file.on('finish', () => {
236
+ file.close();
237
+ resolve({ status: 'cached', location: zipPath });
238
+ });
239
+ }).on('error', () => resolve({ status: 'failed' }));
240
+ });
241
+ }
242
+
243
+ async function installSkill(skillName) {
244
+ const skillsDir = path.join(process.env.HOME, '.openclaw', 'skills');
245
+ const skillDir = path.join(skillsDir, skillName);
246
+
247
+ if (fs.existsSync(skillDir)) {
248
+ return { status: 'exists' };
249
+ }
250
+
251
+ // Fetch from GitHub
252
+ const files = ['SKILL.md', 'config.json'];
253
+ fs.mkdirSync(skillDir, { recursive: true });
254
+
255
+ for (const file of files) {
256
+ const url = `https://raw.githubusercontent.com/skunkceo/openclaw-skills/main/skills/${skillName}/${file}`;
257
+ try {
258
+ const content = await fetchFile(url);
259
+ if (content) {
260
+ fs.writeFileSync(path.join(skillDir, file), content);
261
+ }
262
+ } catch (e) {
263
+ // Optional files may not exist
264
+ }
265
+ }
266
+
267
+ if (fs.existsSync(path.join(skillDir, 'SKILL.md'))) {
268
+ return { status: 'installed' };
269
+ } else {
270
+ fs.rmSync(skillDir, { recursive: true, force: true });
271
+ return { status: 'failed' };
272
+ }
273
+ }
274
+
275
+ function fetchFile(url) {
276
+ return new Promise((resolve, reject) => {
277
+ https.get(url, { headers: { 'User-Agent': 'skunk-cli' } }, (res) => {
278
+ if (res.statusCode === 404) {
279
+ resolve(null);
280
+ return;
281
+ }
282
+ if (res.statusCode !== 200) {
283
+ reject(new Error(`HTTP ${res.statusCode}`));
284
+ return;
285
+ }
286
+ let data = '';
287
+ res.on('data', (chunk) => (data += chunk));
288
+ res.on('end', () => resolve(data));
289
+ }).on('error', reject);
290
+ });
291
+ }
292
+
293
+ // ═══════════════════════════════════════════════════════════════════════════
294
+ // Main Setup Flow
295
+ // ═══════════════════════════════════════════════════════════════════════════
296
+
297
+ async function main() {
298
+ console.clear();
299
+
300
+ // Show splash
301
+ log(colors.bright + SKUNK_LOGO + colors.reset);
302
+ log('');
303
+ log(`${colors.dim}Welcome! Let's get your Skunk suite set up.${colors.reset}`);
304
+ log('');
305
+
306
+ const totalSteps = 5;
307
+
308
+ // ─────────────────────────────────────────────────────────────────────────
309
+ // Step 1: Environment checks
310
+ // ─────────────────────────────────────────────────────────────────────────
311
+ step(1, totalSteps, 'Checking environment...');
312
+
313
+ // Node.js
314
+ const nodeVersion = getVersion('node');
315
+ if (nodeVersion) {
316
+ success(`Node.js ${nodeVersion}`);
317
+ } else {
318
+ error('Node.js not found');
319
+ log('\n Install Node.js: https://nodejs.org/');
320
+ process.exit(1);
321
+ }
322
+
323
+ // npm
324
+ const npmVersion = getVersion('npm');
325
+ if (npmVersion) {
326
+ success(`npm ${npmVersion}`);
327
+ } else {
328
+ error('npm not found');
329
+ process.exit(1);
330
+ }
331
+
332
+ // WP-CLI (optional but recommended)
333
+ let wpVersion = null;
334
+ try {
335
+ wpVersion = execSync('wp --version --allow-root 2>/dev/null || wp --version 2>/dev/null', { encoding: 'utf8' }).trim().split('\n')[0];
336
+ } catch (e) {
337
+ // WP-CLI not installed
338
+ }
339
+ if (wpVersion) {
340
+ success(`WP-CLI ${wpVersion}`);
341
+ } else {
342
+ warn('WP-CLI not found (optional, needed for direct plugin management)');
343
+ }
344
+
345
+ // ─────────────────────────────────────────────────────────────────────────
346
+ // Step 2: WordPress Studio
347
+ // ─────────────────────────────────────────────────────────────────────────
348
+ step(2, totalSteps, 'WordPress Studio...');
349
+
350
+ const studioExists = commandExists('studio');
351
+
352
+ if (studioExists) {
353
+ const studioVersion = getVersion('studio');
354
+ success(`WordPress Studio installed ${studioVersion ? `(${studioVersion})` : ''}`);
355
+ } else {
356
+ error('WordPress Studio not found');
357
+ log('');
358
+ log(' WordPress Studio is required for local WordPress development.');
359
+ log('');
360
+ log(' Install it from: https://developer.wordpress.org/studio/');
361
+ log('');
362
+ log(' On macOS:');
363
+ log(' brew install --cask wordpress-studio');
364
+ log('');
365
+ log(' Or download from the website and install manually.');
366
+ log('');
367
+
368
+ const proceed = await confirm('Continue setup anyway? (skills only)', false);
369
+ if (!proceed) {
370
+ log('\n Run `skunk setup` again after installing WordPress Studio.');
371
+ rl.close();
372
+ process.exit(0);
373
+ }
374
+ }
375
+
376
+ // ─────────────────────────────────────────────────────────────────────────
377
+ // Step 3: Install Plugins
378
+ // ─────────────────────────────────────────────────────────────────────────
379
+ step(3, totalSteps, 'Plugins...');
380
+ log('');
381
+
382
+ // WooCommerce (optional)
383
+ log(` ${colors.bright}WooCommerce${colors.reset}`);
384
+ const wooChoice = await choice(' Install WooCommerce?', [
385
+ { label: 'Yes, include WooCommerce', value: 'yes' },
386
+ { label: 'No, skip WooCommerce', value: 'no' },
387
+ ]);
388
+
389
+ if (wooChoice === 'yes') {
390
+ log(' WooCommerce will be installed when you create a site');
391
+ success('WooCommerce selected');
392
+ } else {
393
+ log(` ${colors.dim}Skipping WooCommerce${colors.reset}`);
394
+ }
395
+ log('');
396
+
397
+ const licenses = {};
398
+
399
+ for (const product of PRODUCTS) {
400
+ log(` ${colors.bright}${product.name}${colors.reset}`);
401
+
402
+ const licenseChoice = await choice(` Do you have a ${product.name} Pro license?`, [
403
+ { label: 'No, install free version', value: 'free' },
404
+ { label: 'Yes, enter license key', value: 'pro' },
405
+ { label: 'Skip for now', value: 'skip' },
406
+ ]);
407
+
408
+ if (licenseChoice === 'skip') {
409
+ warn(` Skipping ${product.name}`);
410
+ continue;
411
+ }
412
+
413
+ if (licenseChoice === 'pro') {
414
+ const key = await ask(' License key: ');
415
+ if (key) {
416
+ log(' Validating license...');
417
+ const result = await validateLicense(key, product.proSlug);
418
+
419
+ if (result.valid) {
420
+ success(`License valid (${result.activationsLeft} activations remaining)`);
421
+ licenses[product.slug] = key;
422
+ // Install both free and pro
423
+ await installPlugin(product.freeRepo, product.slug);
424
+ success(`${product.name} (free) ready`);
425
+ // TODO: Download pro from update server with license
426
+ success(`${product.name} Pro ready`);
427
+ } else {
428
+ warn(`License invalid: ${result.error}`);
429
+ warn('Installing free version instead');
430
+ await installPlugin(product.freeRepo, product.slug);
431
+ success(`${product.name} (free) ready`);
432
+ }
433
+ }
434
+ } else {
435
+ // Free version
436
+ await installPlugin(product.freeRepo, product.slug);
437
+ success(`${product.name} (free) ready`);
438
+ }
439
+
440
+ log('');
441
+ }
442
+
443
+ // ─────────────────────────────────────────────────────────────────────────
444
+ // Step 4: Install Skills
445
+ // ─────────────────────────────────────────────────────────────────────────
446
+ step(4, totalSteps, 'AI Skills...');
447
+
448
+ const skills = ['wordpress-studio', 'woocommerce-manager', 'skunkcrm', 'skunkforms', 'skunkpages'];
449
+
450
+ for (const skill of skills) {
451
+ process.stdout.write(` ${skill} `);
452
+ const result = await installSkill(skill);
453
+ if (result.status === 'installed') {
454
+ log(`${colors.green}✓${colors.reset}`);
455
+ } else if (result.status === 'exists') {
456
+ log(`${colors.dim}(already installed)${colors.reset}`);
457
+ } else {
458
+ log(`${colors.red}✗${colors.reset}`);
459
+ }
460
+ }
461
+
462
+ // ─────────────────────────────────────────────────────────────────────────
463
+ // Step 5: Done!
464
+ // ─────────────────────────────────────────────────────────────────────────
465
+ step(5, totalSteps, 'Setup complete!');
466
+ log('');
467
+ log(` ${colors.green}You're all set!${colors.reset}`);
468
+ log('');
469
+ log(' Next steps:');
470
+ log('');
471
+ if (studioExists) {
472
+ log(' 1. Create a WordPress site:');
473
+ log(' studio create my-site');
474
+ log('');
475
+ log(' 2. Activate Skunk plugins in WordPress admin');
476
+ log('');
477
+ }
478
+ log(' 3. Start chatting with your AI assistant');
479
+ log('');
480
+ log(` ${colors.dim}Docs: https://skunkglobal.com/guides/openclaw-wordpress${colors.reset}`);
481
+ log('');
482
+
483
+ rl.close();
484
+ }
485
+
486
+ // Run
487
+ main().catch((err) => {
488
+ console.error('Setup failed:', err.message);
489
+ process.exit(1);
490
+ });
package/bin/skunk.js CHANGED
@@ -14,6 +14,7 @@ const commands = {
14
14
  list: listSkills,
15
15
  available: listAvailable,
16
16
  remove: removeSkill,
17
+ setup: runSetup,
17
18
  help: showHelp,
18
19
  };
19
20
 
@@ -28,11 +29,17 @@ if (commands[command]) {
28
29
  showHelp();
29
30
  }
30
31
 
32
+ function runSetup() {
33
+ const setupPath = path.join(__dirname, 'setup.js');
34
+ require(setupPath);
35
+ }
36
+
31
37
  function showHelp() {
32
38
  console.log(`
33
- 🦨 Skunk CLI - Install skills for OpenClaw
39
+ 🦨 Skunk CLI - Skunk Global Suite for OpenClaw
34
40
 
35
41
  Usage:
42
+ skunk setup Interactive setup wizard (start here!)
36
43
  skunk install <skill> Install a skill
37
44
  skunk remove <skill> Remove an installed skill
38
45
  skunk list List installed skills
@@ -40,11 +47,11 @@ Usage:
40
47
  skunk help Show this help
41
48
 
42
49
  Examples:
43
- skunk install wordpress-studio
44
- skunk install seo-analyzer
45
- skunk list
50
+ skunk setup # Full suite setup wizard
51
+ skunk install wordpress-studio # Install individual skill
52
+ skunk list # See what's installed
46
53
 
47
- Skills: https://github.com/skunkceo/openclaw-skills
54
+ Docs: https://skunkglobal.com/guides/openclaw-wordpress
48
55
  `);
49
56
  }
50
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skunkceo/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Install and manage Skunk Global skills for OpenClaw",
5
5
  "bin": {
6
6
  "skunk": "./bin/skunk.js"
@@ -9,7 +9,13 @@
9
9
  "type": "git",
10
10
  "url": "https://github.com/skunkceo/skunk-cli"
11
11
  },
12
- "keywords": ["openclaw", "skills", "wordpress", "ai", "skunk"],
12
+ "keywords": [
13
+ "openclaw",
14
+ "skills",
15
+ "wordpress",
16
+ "ai",
17
+ "skunk"
18
+ ],
13
19
  "author": "Skunk Global",
14
20
  "license": "MIT",
15
21
  "homepage": "https://skunkglobal.com/skills"