wp-uikit-cli 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/index.js ADDED
@@ -0,0 +1,1024 @@
1
+ #! /usr/bin/env node
2
+ import { Command } from "commander";
3
+ import inquirer from "inquirer";
4
+ import fs from "fs-extra";
5
+ import path from "path";
6
+ import ora from "ora";
7
+ import figlet from "figlet";
8
+ import chalk from "chalk";
9
+ import { fileURLToPath } from "url";
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const pkg = fs.readJsonSync(path.join(__dirname, "package.json"));
14
+ const program = new Command();
15
+ const traitRegistryPath = path.join(__dirname, "traits.json");
16
+
17
+ program
18
+ .name("sf")
19
+ .description("⚡ WP-UIkit CLI — scaffold, clean, debug and deploy SF Framework / SF Widget plugins")
20
+ .version(pkg.version, "-v, --version", "output the current version");
21
+
22
+
23
+ /* ----------------------------- Zip Starts ----------------------------- */
24
+ async function zipCurrentDirectory(outputName) {
25
+ const spinner = ora("📦 Creating zip...").start();
26
+ const { default: AdmZip } = await import("adm-zip");
27
+ const zip = new AdmZip();
28
+ const root = process.cwd();
29
+
30
+ zip.addLocalFolder(root);
31
+
32
+ const outputPath = path.join(
33
+ root,
34
+ outputName.endsWith(".zip") ? outputName : `${outputName}.zip`
35
+ );
36
+ zip.writeZip(outputPath);
37
+
38
+ spinner.succeed(`✅ Zipped to ${outputPath}`);
39
+ }
40
+ /* ----------------------------- Zip Ends----------------------------- */
41
+
42
+ /* ----------------------------- Create Feature Starts ---------------------------- */
43
+
44
+ // Kebab-case slug for filenames and method suffixes: FancyAnimatedTextTrait -> fancy-animated-text
45
+ function getTraitSlugKebab(traitName) {
46
+ return traitName
47
+ .replace(/Trait$/, "")
48
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
49
+ .replace(/[\s_]+/g, "-")
50
+ .toLowerCase();
51
+ }
52
+
53
+ // Snake-case slug for method names: fancy_animated_text
54
+ function toSnake(slugKebab) {
55
+ return slugKebab.replace(/-/g, "_");
56
+ }
57
+
58
+ // Extract raw args string from a PHP function declaration
59
+ function extractMethodArgsString(fileContent, methodName) {
60
+ const regex = new RegExp(`function\\s+${methodName}\\s*\\(([^)]*)\\)`, "m");
61
+ const match = fileContent.match(regex);
62
+ return match ? match[1].trim() : "";
63
+ }
64
+
65
+ // Parse PHP params into { name, default } items
66
+ function parsePhpParams(paramsStr) {
67
+ if (!paramsStr) return [];
68
+ return paramsStr
69
+ .split(",")
70
+ .map((s) => s.trim())
71
+ .filter(Boolean)
72
+ .map((p) => {
73
+ let cleaned = p
74
+ .replace(/^\s*(?:\[.*?\]\s*)?/, "") // attributes
75
+ .replace(/^\s*&\s*/, "") // by-ref
76
+ .replace(/^\s*(?:\??[A-Za-z_\\][A-Za-z0-9_\\]*)\s+/, ""); // type hints
77
+ const m = cleaned.match(/^\$[A-Za-z_][A-Za-z0-9_]*\s*(?:=\s*(.*))?$/);
78
+ const nameMatch = cleaned.match(/^\$[A-Za-z_][A-Za-z0-9_]*/);
79
+ const name = nameMatch ? nameMatch[0] : "";
80
+ const def = m && m[1] !== undefined ? m[1].trim() : undefined;
81
+ return { name, default: def };
82
+ });
83
+ }
84
+
85
+ // Format arguments for call site - values only (no named params, no assignments)
86
+ function formatCallArgs(params) {
87
+ if (params.length === 0) return "";
88
+ const values = params.map(({ name, default: def }) => {
89
+ if (def !== undefined && def !== "") return def; // keep default literal
90
+ // try variable name (e.g., $prefix) else pass empty string
91
+ return name || "''";
92
+ });
93
+ return values.join(", ");
94
+ }
95
+
96
+ // Build calls by reading trait file, supporting hyphenated filenames and snake-case methods
97
+ function buildTraitCalls(traitName, root) {
98
+ const kebab = getTraitSlugKebab(traitName); // fancy-animated-text
99
+ const snake = toSnake(kebab); // fancy_animated_text
100
+
101
+ const traitFilePath = path.join(
102
+ root,
103
+ "core",
104
+ "includes",
105
+ "traits",
106
+ `${kebab}-trait.php`
107
+ );
108
+
109
+ if (!fs.existsSync(traitFilePath)) {
110
+ console.log(chalk.red(`❌ Trait file not found: ${traitFilePath}`));
111
+ return { controlCall: "", renderCall: "" };
112
+ }
113
+
114
+ const code = fs.readFileSync(traitFilePath, "utf8");
115
+
116
+ // Try standard names first
117
+ let registerSig = extractMethodArgsString(code, `register_${snake}_controls`);
118
+ let renderSig =
119
+ extractMethodArgsString(code, `render_${snake}`) ||
120
+ extractMethodArgsString(
121
+ code,
122
+ `render${snake.replace(/_([a-z])/g, (_, c) => c.toUpperCase())}`
123
+ );
124
+
125
+ // Fallbacks: if trait uses custom naming like fancy_text_register_controls
126
+ if (!registerSig) {
127
+ // try any register_*controls that contains snake parts
128
+ const regAny = code.match(
129
+ new RegExp(
130
+ `function\\s+(register_[A-Za-z0-9_]+controls)\\s*\\(([^)]*)\\)`,
131
+ "g"
132
+ )
133
+ );
134
+ // naive fallback: try specific known variant samples
135
+ registerSig =
136
+ extractMethodArgsString(code, "fancy_text_register_controls") || "";
137
+ }
138
+
139
+ const regParams = parsePhpParams(registerSig);
140
+ const renParams = parsePhpParams(renderSig);
141
+
142
+ const controlArgs = formatCallArgs(regParams);
143
+ const renderArgs = formatCallArgs(renParams);
144
+
145
+ return {
146
+ controlCall: `$this->register_${snake}_controls(${controlArgs});`,
147
+ renderCall: `$this->render_${snake}(${renderArgs});`,
148
+ };
149
+ }
150
+
151
+ /* ---------------------- file/stub helpers ---------------------- */
152
+ function readStub(name) {
153
+ return fs.readFileSync(path.join(__dirname, "stubs", name), "utf8");
154
+ }
155
+
156
+ function writeFromStub(stubName, destPath, replacements = {}) {
157
+ let content = readStub(stubName);
158
+ for (const [k, v] of Object.entries(replacements)) {
159
+ const re = new RegExp(`{{${k}}}`, "g");
160
+ content = content.replace(re, v || "");
161
+ }
162
+ content = content.replace(/{{}}/g, "");
163
+ content = content.replace(/\/\*__([a-z0-9_-]+)__\*\//gi, "");
164
+ content = content.replace(/^[ \t]*\/\*__([a-z0-9_-]+)__\*\/[ \t]*\n?/gim, "");
165
+ fs.outputFileSync(destPath, content);
166
+ }
167
+
168
+ /* --------------------------- sync ------------------------------ */
169
+ async function syncTraits() {
170
+ const traitDir = path.join(process.cwd(), "core", "includes", "traits");
171
+ const traitFiles = fs.existsSync(traitDir)
172
+ ? fs.readdirSync(traitDir).filter((f) => f.endsWith("-trait.php"))
173
+ : [];
174
+
175
+ // Convert filename (kebab) to class name CamelCase + Trait
176
+ const traitNames = traitFiles.map((f) => {
177
+ const base = f.replace("-trait.php", ""); // e.g. fancy-animated-text
178
+ const classish = base
179
+ .split(/[-_ ]+/)
180
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
181
+ .join("");
182
+ return `${classish}Trait`;
183
+ });
184
+
185
+ fs.writeJsonSync(traitRegistryPath, traitNames, { spaces: 2 });
186
+ return traitNames;
187
+ }
188
+
189
+ /* --------------------------- create ---------------------------- */
190
+ async function runCreate(opts) {
191
+ const spinner = ora("Generating widget files...").start();
192
+ const words = opts.name.trim().split(/[\s_-]+/);
193
+ const className = words
194
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
195
+ .join("_");
196
+ const fileName = words.map((w) => w.toLowerCase()).join("-");
197
+ const slug = "sf-" + words.map((w) => w.toLowerCase()).join("-");
198
+ const widgetName =
199
+ "SF " +
200
+ words
201
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
202
+ .join(" ");
203
+ const root = process.cwd();
204
+
205
+ const paths = {
206
+ php: path.join(root, "widgets", `${fileName}.php`),
207
+ css: path.join(root, "assets", "css", `${fileName}.css`),
208
+ js: path.join(root, "assets", "js", `${fileName}.js`),
209
+ };
210
+
211
+ // Smart trait imports and calls
212
+ const traitUses = (opts.traits || [])
213
+ .map((t) => `use \\SFWPStudio\\Core\\Includes\\Traits\\${t};`)
214
+ .join("\n");
215
+
216
+ const traitControls = (opts.traits || [])
217
+ .map((t) => buildTraitCalls(t, root).controlCall)
218
+ .filter(Boolean)
219
+ .join("\n ");
220
+
221
+ const traitRender = (opts.traits || [])
222
+ .map((t) => buildTraitCalls(t, root).renderCall)
223
+ .filter(Boolean)
224
+ .join("\n ");
225
+
226
+ const replacements = {
227
+ CLASSNAME: className,
228
+ FILENAME: fileName,
229
+ SLUG: slug,
230
+ ICON: opts.icon,
231
+ STYLE_DEPENDS: opts.css
232
+ ? `public function get_style_depends() {\n return [ 'sf-${fileName}' ];\n}`
233
+ : "",
234
+ SCRIPT_DEPENDS: opts.js
235
+ ? `public function get_script_depends() {\n return [ 'sf-${fileName}' ];\n}`
236
+ : "",
237
+ TRAIT_USES: traitUses || "",
238
+ TRAIT_IMPORTS: traitUses || "",
239
+ TRAIT_CONTROLS: traitControls || "",
240
+ TRAIT_RENDER: traitRender || "",
241
+ WIDGET_NAME: widgetName || "",
242
+ };
243
+
244
+ const existing = Object.entries(paths)
245
+ .filter(([key, file]) => opts[key] !== false && fs.existsSync(file))
246
+ .map(([_, file]) => file);
247
+
248
+ if (existing.length) {
249
+ spinner.stop();
250
+ console.log(chalk.yellow("The following files already exist:"));
251
+ existing.forEach((f) => console.log(" " + f));
252
+ const { overwrite } = await inquirer.prompt([
253
+ {
254
+ type: "confirm",
255
+ name: "overwrite",
256
+ message: "Overwrite existing files?",
257
+ default: false,
258
+ },
259
+ ]);
260
+ if (!overwrite) {
261
+ console.log(chalk.red("Aborted — no files changed."));
262
+ return;
263
+ }
264
+ spinner.start();
265
+ }
266
+
267
+ writeFromStub("widget.php.stub", paths.php, replacements);
268
+ if (opts.css) writeFromStub("style.css.stub", paths.css, replacements);
269
+ if (opts.js) writeFromStub("script.js.stub", paths.js, replacements);
270
+
271
+ spinner.succeed(chalk.green(`Widget ${className} created!`));
272
+ console.log(chalk.blue("📁 Files created:"));
273
+ Object.entries(paths).forEach(([key, file]) => {
274
+ if (opts[key] !== false) console.log(" " + file);
275
+ });
276
+ }
277
+
278
+ /* --------------------------- trait create ---------------------- */
279
+ function formatTraitNames(input) {
280
+ const raw = input.trim().replace(/Trait$/, "");
281
+ const className =
282
+ raw
283
+ .replace(/[\s_-]+/g, " ")
284
+ .split(" ")
285
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
286
+ .join("") + "Trait";
287
+
288
+ const slug = raw
289
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
290
+ .replace(/[\s\-]+/g, "_")
291
+ .toLowerCase();
292
+
293
+ const fileSlug = raw
294
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
295
+ .replace(/[\s_]+/g, "-")
296
+ .toLowerCase();
297
+
298
+ return { className, slug, fileName: `${fileSlug}-trait.php` };
299
+ }
300
+
301
+ async function runTraitCreation(name) {
302
+ const spinner = ora("Creating trait...").start();
303
+ const { className, slug, fileName } = formatTraitNames(name);
304
+ const traitDir = path.join(process.cwd(), "core", "includes", "traits");
305
+ const traitPath = path.join(traitDir, fileName);
306
+
307
+ if (fs.existsSync(traitPath)) {
308
+ spinner.stop();
309
+ console.log(chalk.yellow(`Trait ${fileName} already exists.`));
310
+ const { overwrite } = await inquirer.prompt([
311
+ {
312
+ type: "confirm",
313
+ name: "overwrite",
314
+ message: "Do you want to overwrite it?",
315
+ default: false,
316
+ },
317
+ ]);
318
+ if (!overwrite) {
319
+ console.log(chalk.red("Trait creation cancelled."));
320
+ return;
321
+ }
322
+ spinner.start();
323
+ }
324
+
325
+ fs.ensureDirSync(traitDir);
326
+ writeFromStub("trait.php.stub", traitPath, {
327
+ TRAITNAME: className,
328
+ TRAITSLUG: slug,
329
+ });
330
+ spinner.succeed(chalk.green(`Trait ${className} created!`));
331
+ console.log(chalk.blue("File created:\n ") + traitPath);
332
+ }
333
+
334
+ async function interactiveFlow(traitChoices) {
335
+ const answers = await inquirer.prompt([
336
+ {
337
+ type: "input",
338
+ name: "name",
339
+ message: "Widget name (e.g. Heading, Icon Box):",
340
+ validate: (val) => !!val || "Name required",
341
+ },
342
+ {
343
+ type: "confirm",
344
+ name: "css",
345
+ message: "Create CSS file?",
346
+ default: true,
347
+ },
348
+ { type: "confirm", name: "js", message: "Create JS file?", default: false },
349
+ {
350
+ type: "input",
351
+ name: "icon",
352
+ message:
353
+ "Elementor icon class (e.g. eicon-star):\nBrowse icons here 👉 (https://elementor.github.io/elementor-icons/):",
354
+ default: "eicon-star",
355
+ },
356
+ {
357
+ type: "checkbox",
358
+ name: "traits",
359
+ message: "Select traits to include:",
360
+ choices: traitChoices,
361
+ },
362
+ ]);
363
+ return answers;
364
+ }
365
+ /* ----------------------------- Create Feature Ends ---------------------------- */
366
+
367
+ /* ----------------------------- Debug Feature Starts ---------------------------- */
368
+
369
+ function toggleDebugMode(enable) {
370
+ const currentDir = process.cwd();
371
+ let wpConfigPath = "";
372
+ let dir = currentDir;
373
+
374
+ while (dir !== path.parse(dir).root) {
375
+ const candidate = path.join(dir, "wp-config.php");
376
+ if (fs.existsSync(candidate)) {
377
+ wpConfigPath = candidate;
378
+ break;
379
+ }
380
+ dir = path.dirname(dir);
381
+ }
382
+
383
+ if (!wpConfigPath) {
384
+ console.log(
385
+ chalk.red(
386
+ "❌ wp-config.php not found — make sure you’re inside a plugin/widget directory"
387
+ )
388
+ );
389
+ return;
390
+ }
391
+
392
+ let content = fs.readFileSync(wpConfigPath, "utf8");
393
+ let modified = false;
394
+
395
+ const wpDebugRegex = /define\(\s*['"]WP_DEBUG['"]\s*,\s*(true|false)\s*\);/;
396
+ const wpDebugReplacement = `define('WP_DEBUG', ${enable});`;
397
+
398
+ const newContent = content.replace(wpDebugRegex, wpDebugReplacement);
399
+ if (newContent !== content) {
400
+ content = newContent;
401
+ modified = true;
402
+ }
403
+
404
+ const debugBlock = `
405
+ if ( ! defined( 'WP_DEBUG_LOG' ) ) {
406
+ define( 'WP_DEBUG_LOG', true );
407
+ }
408
+
409
+ if ( ! defined( 'WP_DEBUG_DISPLAY' ) ) {
410
+ define( 'WP_DEBUG_DISPLAY', true );
411
+ }
412
+ `.trim();
413
+
414
+ if (enable) {
415
+ if (content.includes(debugBlock)) {
416
+ console.log(chalk.yellow("⚠️ Debug block already present"));
417
+ } else {
418
+ const wpDebugBlockRegex =
419
+ /if\s*\(\s*!\s*defined\s*\(\s*['"]WP_DEBUG['"]\s*\)\s*\)\s*\{\s*define\s*\(\s*['"]WP_DEBUG['"]\s*,\s*true\s*\)\s*;\s*\}/s;
420
+ const wpDebugLineRegex =
421
+ /define\(\s*['"]WP_DEBUG['"]\s*,\s*true\s*\);\s*/;
422
+
423
+ let updatedContent = content.replace(
424
+ wpDebugBlockRegex,
425
+ `if ( ! defined( 'WP_DEBUG' ) ) {\n\tdefine( 'WP_DEBUG', true );\n}\n\n${debugBlock}\n`
426
+ );
427
+
428
+ if (updatedContent === content) {
429
+ updatedContent = content.replace(
430
+ wpDebugLineRegex,
431
+ `define('WP_DEBUG', true);\n\n${debugBlock}\n`
432
+ );
433
+ }
434
+
435
+ if (updatedContent !== content) {
436
+ content = updatedContent;
437
+ modified = true;
438
+ } else {
439
+ console.log(
440
+ chalk.red("❌ WP_DEBUG line not found — cannot insert debug block")
441
+ );
442
+ return;
443
+ }
444
+ }
445
+ } else {
446
+ const blockRegex = new RegExp(
447
+ `\\n?if\\s*\\(\\s*!\\s*defined\\s*\\(\\s*['"]WP_DEBUG_LOG['"]\\s*\\)\\s*\\)\\s*{[^}]*}\\s*\\n?if\\s*\\(\\s*!\\s*defined\\s*\\(\\s*['"]WP_DEBUG_DISPLAY['"]\\s*\\)\\s*\\)\\s*{[^}]*}`,
448
+ "g"
449
+ );
450
+ const cleanedContent = content.replace(blockRegex, "").trim();
451
+ if (cleanedContent !== content) {
452
+ content = cleanedContent;
453
+ modified = true;
454
+ }
455
+ }
456
+
457
+ if (modified) {
458
+ fs.writeFileSync(wpConfigPath, content + "\n");
459
+ console.log(
460
+ chalk.green(
461
+ `✅ Debug mode ${
462
+ enable ? "activated" : "deactivated"
463
+ } and config updated`
464
+ )
465
+ );
466
+ if (enable) {
467
+ console.log(
468
+ chalk.blue("\n📣 To verify logging, paste this in any PHP file:")
469
+ );
470
+ console.log(
471
+ chalk.yellow(`\n error_log(print_r("Welcome to WP-UIkit Debug", true));\n`)
472
+ );
473
+ console.log(
474
+ chalk.white(`Then check:`),
475
+ chalk.cyan(`wp-content/debug.log\n`)
476
+ );
477
+ }
478
+ }
479
+ }
480
+ /* ----------------------------- Debug Feature Ends ---------------------------- */
481
+
482
+ /* ----------------------------- Code Clean Feature Starts ---------------------------- */
483
+
484
+ // Process single widget and return statistics
485
+ async function localizeWidgetStrings(widgetName) {
486
+ const root = process.cwd();
487
+ const fileSlug = widgetName.trim().toLowerCase().replace(/\s+/g, "-");
488
+ const phpPath = path.join(root, "widgets", `${fileSlug}.php`);
489
+
490
+ if (!fs.existsSync(phpPath)) {
491
+ return { widget: widgetName, found: false, escHtmlCount: 0, domain: "-" };
492
+ }
493
+
494
+ let content = fs.readFileSync(phpPath, "utf8");
495
+ const originalContent = content;
496
+ let escHtmlCount = 0;
497
+
498
+ // 1️⃣ Normalize domain inside existing esc_html__ calls
499
+ content = content.replace(
500
+ /esc_html__\(\s*(['"])([^'"]+)\1\s*,\s*['"][^'"]+['"]\s*\)/g,
501
+ (match, q, str) => {
502
+ escHtmlCount++;
503
+ return `esc_html__(${q}${str}${q}, 'sf-widget')`;
504
+ }
505
+ );
506
+
507
+ // 2️⃣ Upgrade existing __(...) calls to esc_html__()
508
+ content = content.replace(
509
+ /__\(\s*(['"])([^'"]+)\1\s*,\s*['"][^'"]+['"]\s*\)/g,
510
+ (match, q, str) => {
511
+ escHtmlCount++;
512
+ return `esc_html__(${q}${str}${q}, 'sf-widget')`;
513
+ }
514
+ );
515
+
516
+ // 3️⃣ Wrap plain strings in esc_html__()
517
+ const regex =
518
+ /(label|description|title|placeholder|raw)\s*=>\s*['"]([^'"]+)['"]/g;
519
+ content = content.replace(regex, (match, key, str) => {
520
+ if (/esc_html__\(/.test(match)) return match;
521
+ escHtmlCount++;
522
+ return `${key} => esc_html__('${str}', 'sf-widget')`;
523
+ });
524
+
525
+ // 4️⃣ Cleanup accidental double replacements
526
+ content = content.replace(/esc_htmlesc_html__/g, "esc_html__");
527
+
528
+ if (content !== originalContent) {
529
+ fs.writeFileSync(phpPath, content);
530
+ }
531
+
532
+ return {
533
+ widget: widgetName,
534
+ found: true,
535
+ escHtmlCount: escHtmlCount,
536
+ domain: "sf-widget",
537
+ path: phpPath,
538
+ };
539
+ }
540
+
541
+ // Process all widgets
542
+ async function localizeAllWidgets() {
543
+ const root = process.cwd();
544
+ const widgetsDir = path.join(root, "widgets");
545
+
546
+ if (!fs.existsSync(widgetsDir)) {
547
+ console.log(chalk.red("❌ Widgets directory not found"));
548
+ return;
549
+ }
550
+
551
+ const widgetFiles = fs.readdirSync(widgetsDir).filter((f) => f.endsWith(".php"));
552
+
553
+ if (widgetFiles.length === 0) {
554
+ console.log(chalk.yellow("⚠️ No widget files found"));
555
+ return;
556
+ }
557
+
558
+ const spinner = ora(`🌐 Processing ${widgetFiles.length} widgets...`).start();
559
+ const results = [];
560
+
561
+ for (const file of widgetFiles) {
562
+ const widgetName = file.replace(".php", "");
563
+ const result = await localizeWidgetStrings(widgetName);
564
+ results.push(result);
565
+ }
566
+
567
+ spinner.succeed(`✅ Processed ${widgetFiles.length} widgets\n`);
568
+
569
+ // Display results in table format
570
+ console.log(chalk.bold.cyan("\n📊 Localization Report\n"));
571
+
572
+ const header = [
573
+ chalk.bold.white("Widget"),
574
+ chalk.bold.white("Status"),
575
+ chalk.bold.white("esc_html Count"),
576
+ chalk.bold.white("Domain"),
577
+ ];
578
+
579
+ const maxWidgetLen = Math.max(...results.map((r) => r.widget.length), "Widget".length);
580
+ const maxStatusLen = 9;
581
+ const maxCountLen = 14;
582
+ const maxDomainLen = 10;
583
+
584
+ console.log(
585
+ chalk.gray(
586
+ "├─ " +
587
+ "─".repeat(maxWidgetLen + 2) +
588
+ "─".repeat(maxStatusLen + 2) +
589
+ "─".repeat(maxCountLen + 2) +
590
+ "─".repeat(maxDomainLen)
591
+ )
592
+ );
593
+
594
+ // Print header
595
+ console.log(
596
+ chalk.gray("│ ") +
597
+ "Widget".padEnd(maxWidgetLen) +
598
+ chalk.gray(" │ ") +
599
+ "Status".padEnd(maxStatusLen) +
600
+ chalk.gray(" │ ") +
601
+ "esc_html Count".padEnd(maxCountLen) +
602
+ chalk.gray(" │ ") +
603
+ "Domain".padEnd(maxDomainLen)
604
+ );
605
+
606
+ console.log(
607
+ chalk.gray(
608
+ "├─ " +
609
+ "─".repeat(maxWidgetLen + 2) +
610
+ "─".repeat(maxStatusLen + 2) +
611
+ "─".repeat(maxCountLen + 2) +
612
+ "─".repeat(maxDomainLen)
613
+ )
614
+ );
615
+
616
+ // Print rows
617
+ results.forEach((result) => {
618
+ const status = result.found ? chalk.green("✓ Done") : chalk.red("✗ Missing");
619
+ const count = result.escHtmlCount > 0 ? chalk.yellow(result.escHtmlCount) : chalk.gray("0");
620
+ const domain = result.found ? chalk.cyan(result.domain) : chalk.gray("-");
621
+
622
+ console.log(
623
+ chalk.gray("│ ") +
624
+ result.widget.padEnd(maxWidgetLen) +
625
+ chalk.gray(" │ ") +
626
+ status.padEnd(maxStatusLen + 10) +
627
+ chalk.gray(" │ ") +
628
+ count.padEnd(maxCountLen + 10) +
629
+ chalk.gray(" │ ") +
630
+ domain.padEnd(maxDomainLen + 5)
631
+ );
632
+ });
633
+
634
+ console.log(
635
+ chalk.gray(
636
+ "└─ " +
637
+ "─".repeat(maxWidgetLen + 2) +
638
+ "─".repeat(maxStatusLen + 2) +
639
+ "─".repeat(maxCountLen + 2) +
640
+ "─".repeat(maxDomainLen)
641
+ )
642
+ );
643
+
644
+ // Summary
645
+ const totalCount = results.reduce((sum, r) => sum + r.escHtmlCount, 0);
646
+ const processedCount = results.filter((r) => r.found).length;
647
+
648
+ console.log(chalk.bold.cyan(`\n✨ Summary`));
649
+ console.log(chalk.gray(` ├─ Total widgets processed: `) + chalk.white(processedCount));
650
+ console.log(chalk.gray(` ├─ Total esc_html__() added: `) + chalk.yellow(totalCount));
651
+ console.log(chalk.gray(` └─ Domain used: `) + chalk.cyan("sf-widget\n"));
652
+ }
653
+
654
+ /* ----------------------------- Code Clean Feature Ends ---------------------------- */
655
+
656
+
657
+
658
+ /* ----------------------------- Commands Starts ---------------------------- */
659
+ program
660
+ .command("debug <mode>")
661
+ .description("Toggle WP_DEBUG settings in wp-config.php (appended at end)")
662
+ .action((mode) => {
663
+ const enable = mode === "on";
664
+ toggleDebugMode(enable);
665
+ });
666
+
667
+ program
668
+ .command("clean [widgetName]")
669
+ .option("--all", "Process all widgets with localization statistics")
670
+ .description("Wrap all user-visible strings in esc_html__() with domain 'sf-widget'")
671
+ .action(async (widgetName, opts) => {
672
+ if (opts.all) {
673
+ await localizeAllWidgets();
674
+ } else if (widgetName) {
675
+ const spinner = ora(`🌐 Localizing strings for ${widgetName}...`).start();
676
+ const result = await localizeWidgetStrings(widgetName);
677
+ if (result.found) {
678
+ spinner.succeed(
679
+ `✅ Localized ${result.escHtmlCount} strings in ${widgetName} with esc_html__() and domain 'sf-widget'`
680
+ );
681
+ } else {
682
+ spinner.fail(`❌ Widget file not found: ${widgetName}.php`);
683
+ }
684
+ } else {
685
+ console.log(chalk.yellow("⚠️ Please provide a widget name or use --all flag\n"));
686
+ console.log(chalk.cyan("Usage:"));
687
+ console.log(chalk.gray(" sf clean <widgetName> - Process single widget"));
688
+ console.log(chalk.gray(" sf clean --all - Process all widgets"));
689
+ }
690
+ });
691
+
692
+ program
693
+ .command("create")
694
+ .description("Create a widget or trait interactively")
695
+ .action(async () => {
696
+ console.log(
697
+ chalk.cyan(figlet.textSync("WP-UIkit CLI", { horizontalLayout: "full" }))
698
+ );
699
+
700
+ const spinner = ora("🍳 Cooking up trait list...").start();
701
+ await new Promise((resolve) => setTimeout(resolve, 1000));
702
+ const traitChoices = await syncTraits();
703
+ spinner.succeed("Traits synced!");
704
+
705
+ const { type } = await inquirer.prompt([
706
+ {
707
+ type: "list",
708
+ name: "type",
709
+ message: "What do you want to create?",
710
+ choices: ["Widget", "Trait"],
711
+ },
712
+ ]);
713
+
714
+ if (type === "Trait") {
715
+ const { traitName } = await inquirer.prompt([
716
+ {
717
+ type: "input",
718
+ name: "traitName",
719
+ message: "Enter trait name (e.g. CardTrait):",
720
+ validate: (val) => !!val || "Trait name required",
721
+ },
722
+ ]);
723
+ await runTraitCreation(traitName);
724
+ } else {
725
+ const answers = await interactiveFlow(traitChoices);
726
+ await runCreate({ ...answers });
727
+ }
728
+ });
729
+
730
+ program
731
+ .command("zip [filename]")
732
+ .description("Zip the current working directory into <filename>.zip")
733
+ .action(async (filename) => {
734
+ const finalName =
735
+ filename && String(filename).trim()
736
+ ? String(filename).trim()
737
+ : path.basename(process.cwd());
738
+
739
+ await zipCurrentDirectory(finalName);
740
+ });
741
+
742
+ /* ----------------------------- Deploy Feature Starts ---------------------------- */
743
+
744
+ /**
745
+ * Recursively minify all .js files in a directory using Terser.
746
+ * Writes <name>.min.js and deletes the original.
747
+ */
748
+ async function minifyJsFiles(dir, spinner, terserMinify) {
749
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
750
+ for (const entry of entries) {
751
+ const fullPath = path.join(dir, entry.name);
752
+ if (entry.isDirectory()) {
753
+ await minifyJsFiles(fullPath, spinner, terserMinify);
754
+ } else if (
755
+ entry.isFile() &&
756
+ entry.name.endsWith(".js") &&
757
+ !entry.name.endsWith(".min.js")
758
+ ) {
759
+ try {
760
+ const original = fs.readFileSync(fullPath, "utf8");
761
+ const result = await terserMinify(original, { mangle: true, compress: true });
762
+ if (result.code) {
763
+ const minPath = fullPath.replace(/\.js$/, ".min.js");
764
+ fs.writeFileSync(minPath, result.code, "utf8");
765
+ fs.removeSync(fullPath);
766
+ spinner.text = chalk.gray(` Minified JS: ${path.basename(fullPath)}`);
767
+ }
768
+ } catch (err) {
769
+ spinner.warn(chalk.yellow(`⚠️ Terser failed on ${fullPath}: ${err.message}`));
770
+ spinner.start();
771
+ }
772
+ }
773
+ }
774
+ }
775
+
776
+ /**
777
+ * Recursively minify all .css files in a directory using CleanCSS.
778
+ * Writes <name>.min.css and deletes the original.
779
+ */
780
+ function minifyCssFiles(dir, spinner, CleanCSS) {
781
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
782
+ for (const entry of entries) {
783
+ const fullPath = path.join(dir, entry.name);
784
+ if (entry.isDirectory()) {
785
+ minifyCssFiles(fullPath, spinner, CleanCSS);
786
+ } else if (
787
+ entry.isFile() &&
788
+ entry.name.endsWith(".css") &&
789
+ !entry.name.endsWith(".min.css")
790
+ ) {
791
+ try {
792
+ const original = fs.readFileSync(fullPath, "utf8");
793
+ const result = new CleanCSS({}).minify(original);
794
+ if (result.errors && result.errors.length > 0) {
795
+ spinner.warn(chalk.yellow(`⚠️ CleanCSS errors on ${fullPath}: ${result.errors.join(", ")}`));
796
+ spinner.start();
797
+ } else {
798
+ const minPath = fullPath.replace(/\.css$/, ".min.css");
799
+ fs.writeFileSync(minPath, result.styles, "utf8");
800
+ fs.removeSync(fullPath);
801
+ spinner.text = chalk.gray(` Minified CSS: ${path.basename(fullPath)}`);
802
+ }
803
+ } catch (err) {
804
+ spinner.warn(chalk.yellow(`⚠️ CleanCSS failed on ${fullPath}: ${err.message}`));
805
+ spinner.start();
806
+ }
807
+ }
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Main deploy handler. Called with { framework: true } or { widget: true }.
813
+ *
814
+ * Flow:
815
+ * - CWD = the plugin folder → minify, dev flag, gitignore removal, zip
816
+ * - destDir = folder path user provides → zip is moved here + JSON is updated here
817
+ */
818
+ async function runDeploy(opts) {
819
+ const isFramework = !!opts.framework;
820
+ const isWidget = !!opts.widget;
821
+
822
+ if (!isFramework && !isWidget) {
823
+ console.log(chalk.red("❌ Please specify --framework or --widget"));
824
+ return;
825
+ }
826
+
827
+ // ── Plugin meta ──────────────────────────────────────────────────────────
828
+ const pluginKey = isFramework ? "sf-framework" : "sf-widget";
829
+ const mainPhpFile = isFramework ? "sf-framework.php" : "sf-widget.php";
830
+ const devConstant = isFramework ? "SF_FRAMEWORK_DEVELOPMENT_MODE" : "SF_DEVELOPMENT_MODE";
831
+ const downloadUrl = isFramework
832
+ ? `https://gitea.syncfusion.com/syncfusion-wordpress-template/SF-Framework/raw/branch/main/{{VERSION}}`
833
+ : `https://gitea.syncfusion.com/syncfusion-wordpress-template/SF-Widget/raw/branch/development/{{VERSION}}`;
834
+ const description = isFramework
835
+ ? "A Design System for SF Widgets and Elementor. Build modern websites with ease."
836
+ : "30+ widgets Pro Feature and new design system for Elementor. Build modern websites with ease.";
837
+
838
+ // ── Plugin folder = current working directory ────────────────────────────
839
+ const pluginDir = process.cwd();
840
+ const divider = chalk.gray("─".repeat(60));
841
+
842
+ console.log("\n" + chalk.bold.cyan(figlet.textSync("WP-UIkit Deploy", { horizontalLayout: "full" })));
843
+ console.log(divider);
844
+ console.log(chalk.bold(` Plugin `) + chalk.cyan(pluginKey));
845
+ console.log(chalk.bold(` Source `) + chalk.white(pluginDir));
846
+ console.log(divider + "\n");
847
+
848
+ // ── Step 1: Ask for destination folder (where zip + JSON will go) ────────
849
+ const { folderPath } = await inquirer.prompt([
850
+ {
851
+ type: "input",
852
+ name: "folderPath",
853
+ message: chalk.bold("Destination folder") + chalk.gray(" (zip & update.json will be placed here):"),
854
+ validate: (val) => {
855
+ const trimmed = val.trim();
856
+ if (!trimmed) return "Folder path is required";
857
+ if (!fs.existsSync(trimmed)) return `Path does not exist: ${trimmed}`;
858
+ if (!fs.statSync(trimmed).isDirectory()) return "Path must be a directory";
859
+ return true;
860
+ },
861
+ },
862
+ ]);
863
+
864
+ const destDir = path.resolve(folderPath.trim());
865
+
866
+ // ── Step 2: Ask for new version ──────────────────────────────────────────
867
+ const { newVersion } = await inquirer.prompt([
868
+ {
869
+ type: "input",
870
+ name: "newVersion",
871
+ message: chalk.bold("New version") + chalk.gray(" (e.g. 1.2.0):"),
872
+ validate: (val) => /^\d+\.\d+\.\d+$/.test(val.trim()) || "Version must be in X.Y.Z format",
873
+ },
874
+ ]);
875
+
876
+ const version = newVersion.trim();
877
+ const zipName = `${pluginKey}-${version}`;
878
+
879
+ console.log("\n" + divider);
880
+ console.log(chalk.bold.white(" Running build steps"));
881
+ console.log(divider);
882
+
883
+ // ── Step 3: Minify JS assets (in CWD) ───────────────────────────────────
884
+ const jsDir = path.join(pluginDir, "assets", "js");
885
+ if (fs.existsSync(jsDir)) {
886
+ const jsSpinner = ora({ text: chalk.white("Minifying JS files..."), prefixText: " " }).start();
887
+ const { minify: terserMinify } = await import("terser");
888
+ await minifyJsFiles(jsDir, jsSpinner, terserMinify);
889
+ jsSpinner.succeed(chalk.green("JS minified successfully"));
890
+ } else {
891
+ console.log(chalk.gray(" › No assets/js directory — skipping JS minification"));
892
+ }
893
+
894
+ // ── Step 4: Minify CSS assets (in CWD) ──────────────────────────────────
895
+ const cssDir = path.join(pluginDir, "assets", "css");
896
+ if (fs.existsSync(cssDir)) {
897
+ const cssSpinner = ora({ text: chalk.white("Minifying CSS files..."), prefixText: " " }).start();
898
+ const { default: CleanCSS } = await import("clean-css");
899
+ minifyCssFiles(cssDir, cssSpinner, CleanCSS);
900
+ cssSpinner.succeed(chalk.green("CSS minified successfully"));
901
+ } else {
902
+ console.log(chalk.gray(" › No assets/css directory — skipping CSS minification"));
903
+ }
904
+
905
+ // ── Step 5: Set development flag to false (in CWD) ──────────────────────
906
+ const mainPhpPath = path.join(pluginDir, mainPhpFile);
907
+ if (fs.existsSync(mainPhpPath)) {
908
+ const devSpinner = ora({ text: chalk.white(`Disabling development mode...`), prefixText: " " }).start();
909
+ let phpContent = fs.readFileSync(mainPhpPath, "utf8");
910
+ const devFlagRegex = new RegExp(
911
+ `define\\s*\\(\\s*['"]${devConstant}['"]\\s*,\\s*true\\s*\\)`,
912
+ "g"
913
+ );
914
+ const updated = phpContent.replace(devFlagRegex, `define('${devConstant}', false)`);
915
+ if (updated !== phpContent) {
916
+ fs.writeFileSync(mainPhpPath, updated, "utf8");
917
+ devSpinner.succeed(chalk.green(`Development mode disabled `) + chalk.gray(`(${mainPhpFile})`));
918
+ } else {
919
+ devSpinner.warn(chalk.yellow(`Flag not found or already false `) + chalk.gray(`(${mainPhpFile})`));
920
+ }
921
+ } else {
922
+ console.log(chalk.yellow(` › ${mainPhpFile} not found — skipping dev flag update`));
923
+ }
924
+
925
+ // ── Step 6: Remove .gitignore (in CWD) ──────────────────────────────────
926
+ const gitignorePath = path.join(pluginDir, ".gitignore");
927
+ if (fs.existsSync(gitignorePath)) {
928
+ fs.removeSync(gitignorePath);
929
+ console.log(" " + chalk.green("✔") + " " + chalk.green(".gitignore removed"));
930
+ } else {
931
+ console.log(chalk.gray(" › No .gitignore found — skipping"));
932
+ }
933
+
934
+ // ── Step 6.5: Remove fonts folder for widget plugin (in CWD) ─────────────
935
+ if (isWidget) {
936
+ const fontsPath = path.join(pluginDir, "assets", "icons", "fonts");
937
+ if (fs.existsSync(fontsPath)) {
938
+ fs.removeSync(fontsPath);
939
+ console.log(" " + chalk.green("✔") + " " + chalk.green("Fonts folder removed"));
940
+ } else {
941
+ console.log(chalk.gray(" › No fonts folder found — skipping"));
942
+ }
943
+ }
944
+
945
+ // ── Step 7: Zip the CWD plugin folder ───────────────────────────────────
946
+ const zipSpinner = ora({ text: chalk.white(`Creating zip ${zipName}.zip...`), prefixText: " " }).start();
947
+ const { default: AdmZip } = await import("adm-zip");
948
+ const zip = new AdmZip();
949
+
950
+ // Exclude .git, node_modules, and any existing .zip files from the archive
951
+ const excludeDirs = [".git", "node_modules"];
952
+ zip.addLocalFolder(pluginDir, pluginKey, (filePath) => {
953
+ const normalized = filePath.replace(/\\/g, "/");
954
+ for (const dir of excludeDirs) {
955
+ if (normalized.startsWith(`${dir}/`) || normalized === dir) return false;
956
+ }
957
+ if (normalized.endsWith(".zip")) return false;
958
+ return true;
959
+ });
960
+
961
+ const tempZipPath = path.join(pluginDir, `${zipName}.zip`);
962
+ zip.writeZip(tempZipPath);
963
+ zipSpinner.succeed(chalk.green(`Zip created `) + chalk.gray(`${zipName}.zip`));
964
+
965
+ // ── Step 8: Move zip to destination folder ───────────────────────────────
966
+ const finalZipPath = path.join(destDir, `${zipName}.zip`);
967
+ fs.moveSync(tempZipPath, finalZipPath, { overwrite: true });
968
+ console.log(" " + chalk.green("✔") + " " + chalk.green("Zip moved to destination ") + chalk.gray(finalZipPath));
969
+
970
+ // ── Step 9: Update update.json in destination folder (only if it exists) ──
971
+ const jsonFilePath = path.join(destDir, "update.json");
972
+ if (fs.existsSync(jsonFilePath)) {
973
+ const jsonSpinner = ora({ text: chalk.white("Updating update.json..."), prefixText: " " }).start();
974
+ const versionedUrl = downloadUrl.replace("{{VERSION}}", `${zipName}.zip`);
975
+ const jsonData = {
976
+ name: pluginKey,
977
+ version: version,
978
+ download_url: versionedUrl,
979
+ sections: { description: description },
980
+ };
981
+ fs.writeJsonSync(jsonFilePath, jsonData, { spaces: 2 });
982
+ jsonSpinner.succeed(chalk.green("update.json updated ") + chalk.gray(jsonFilePath));
983
+ } else {
984
+ console.log(chalk.gray(" › No update.json found in destination — skipping"));
985
+ }
986
+
987
+ // ── Done ─────────────────────────────────────────────────────────────────
988
+ console.log("\n" + divider);
989
+ console.log(
990
+ chalk.bold.green(" ✔ Deploy complete — ") +
991
+ chalk.bold.white(`${pluginKey}`) +
992
+ chalk.bold.green(" v") +
993
+ chalk.bold.cyan(`${version}`)
994
+ );
995
+ console.log(divider);
996
+ console.log(chalk.bold(`\n Output`));
997
+ console.log(chalk.gray(` ├─ Zip `) + chalk.white(finalZipPath));
998
+ if (fs.existsSync(jsonFilePath)) {
999
+ console.log(chalk.gray(` └─ JSON `) + chalk.white(jsonFilePath));
1000
+ }
1001
+ console.log(chalk.bold(`\n Next steps`));
1002
+ console.log(chalk.gray(` ├─`) + chalk.yellow(` Open the destination folder in your file manager`));
1003
+ console.log(chalk.gray(` ├─`) + chalk.yellow(` Review the changes — check update.json & zip`));
1004
+ console.log(chalk.gray(` ├─`) + chalk.yellow(` Stage all changes: `) + chalk.cyan(`git add .`));
1005
+ console.log(chalk.gray(` ├─`) + chalk.yellow(` Commit: `) + chalk.cyan(`git commit -m "Release ${pluginKey} v${version}"`));
1006
+ console.log(chalk.gray(` └─`) + chalk.yellow(` Push: `) + chalk.cyan(`git push`));
1007
+ console.log("");
1008
+ }
1009
+
1010
+ /* ----------------------------- Deploy Feature Ends ---------------------------- */
1011
+
1012
+ program
1013
+ .command("deploy")
1014
+ .description("Deploy sf-framework or sf-widget plugin (minify, update JSON, zip)")
1015
+ .option("--framework", "Deploy sf-framework plugin")
1016
+ .option("--widget", "Deploy sf-widget plugin")
1017
+ .action(async (opts) => {
1018
+ await runDeploy(opts);
1019
+ });
1020
+
1021
+ program.parse(process.argv);
1022
+
1023
+ /* ----------------------------- Commands Ends ---------------------------- */
1024
+