create-nexa-app 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/bin/nexa.js ADDED
@@ -0,0 +1,1253 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * File: bin/nexa.js
5
+ * Purpose: Main entrypoint for the Nexa CLI. This file parses commands and
6
+ * scaffolds apps, components, services, and contexts for React/Vite projects.
7
+ */
8
+
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import os from "os";
12
+ import readline from "readline";
13
+ import { fileURLToPath } from "url";
14
+ import { execSync, spawn } from "child_process";
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+ const args = process.argv.slice(2);
19
+
20
+ // ✅ READS THE VERSION OF THE CLI
21
+
22
+ const pkg = JSON.parse(
23
+ fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"),
24
+ );
25
+ if (
26
+ args.includes("--version") ||
27
+ args.includes("-v") ||
28
+ args[0] === "version"
29
+ ) {
30
+ const pkg = JSON.parse(
31
+ fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"),
32
+ );
33
+ console.log(`Nexa CLI v${pkg.version}`);
34
+ process.exit(0);
35
+ }
36
+
37
+ /**
38
+ * Terminal colors for Nexa branding.
39
+ */
40
+ const C = {
41
+ reset: "\x1b[0m",
42
+ cyan: "\x1b[36m",
43
+ yellow: "\x1b[33m",
44
+ green: "\x1b[32m",
45
+ blue: "\x1b[34m",
46
+ gray: "\x1b[90m",
47
+ bold: "\x1b[1m",
48
+ };
49
+
50
+ /**
51
+ * Purpose: Convert a string like "my-app" or "my_app" to PascalCase.
52
+ */
53
+ function toPascalCase(str = "") {
54
+ return str
55
+ .trim()
56
+ .split(/[\s-_]+/)
57
+ .filter(Boolean)
58
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
59
+ .join("");
60
+ }
61
+
62
+ /**
63
+ * Purpose: Convert a string to kebab-case for folder/package names.
64
+ */
65
+ function toKebabCase(str = "") {
66
+ return str
67
+ .trim()
68
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
69
+ .split(/[\s_]+/)
70
+ .join("-")
71
+ .replace(/-+/g, "-")
72
+ .toLowerCase();
73
+ }
74
+
75
+ /**
76
+ * Purpose: Convert a string into a safe CSS class prefix.
77
+ */
78
+ function toCssClassName(str = "") {
79
+ return toKebabCase(str).replace(/[^a-z0-9-]/g, "");
80
+ }
81
+
82
+ /**
83
+ * Purpose: Ensure parent directories exist before writing a file.
84
+ */
85
+ function writeFileSafe(filePath, content) {
86
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
87
+ fs.writeFileSync(filePath, content, "utf8");
88
+ }
89
+
90
+ /**
91
+ * Purpose: Recursively copy directories and files, skipping junk folders.
92
+ */
93
+ function copyRecursive(src, dest) {
94
+ if (!fs.existsSync(src)) return;
95
+
96
+ const stat = fs.statSync(src);
97
+ const base = path.basename(src);
98
+
99
+ const IGNORE_DIRS = ["node_modules", ".git"];
100
+ const IGNORE_EXACT_FILES = [".DS_Store"];
101
+ const IGNORE_FILE_PREFIXES = [".env"];
102
+
103
+ if (stat.isDirectory()) {
104
+ if (IGNORE_DIRS.includes(base)) return;
105
+
106
+ fs.mkdirSync(dest, { recursive: true });
107
+
108
+ for (const item of fs.readdirSync(src)) {
109
+ copyRecursive(path.join(src, item), path.join(dest, item));
110
+ }
111
+ } else {
112
+ if (
113
+ IGNORE_EXACT_FILES.includes(base) ||
114
+ IGNORE_FILE_PREFIXES.some(
115
+ (prefix) => base === prefix || base.startsWith(`${prefix}.`),
116
+ )
117
+ ) {
118
+ return;
119
+ }
120
+
121
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
122
+ fs.copyFileSync(src, dest);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Purpose: Make sure generator commands are run from the project root,
128
+ * not from inside src or any nested folder.
129
+ */
130
+ function ensureProjectRootForGenerators() {
131
+ const cwd = process.cwd();
132
+ const normalized = path.normalize(cwd);
133
+ const parts = normalized.split(path.sep).filter(Boolean);
134
+
135
+ const packageJsonPath = path.join(cwd, "package.json");
136
+ const srcPath = path.join(cwd, "src");
137
+
138
+ const isInsideSrc =
139
+ parts[parts.length - 1] === "src" ||
140
+ normalized.includes(`${path.sep}src${path.sep}`);
141
+
142
+ if (isInsideSrc) {
143
+ console.error(
144
+ `${C.yellow}❌ Run Nexa generator commands from the project root, not from inside src.${C.reset}`,
145
+ );
146
+ console.error(
147
+ `${C.green}✅ Example: run 'nexa new gc MyComponent' from the app root.${C.reset}`,
148
+ );
149
+ process.exit(1);
150
+ }
151
+
152
+ if (!fs.existsSync(packageJsonPath) || !fs.existsSync(srcPath)) {
153
+ console.error(
154
+ `${C.yellow}❌ Nexa generator commands must be run from the project root folder.${C.reset}`,
155
+ );
156
+ console.error(
157
+ `${C.green}✅ Expected to find both package.json and src/ in the current directory.${C.reset}`,
158
+ );
159
+ process.exit(1);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Purpose: Print usage help.
165
+ */
166
+ function printUsage() {
167
+ console.log(`
168
+ ${C.cyan}${C.bold}🚀 Nexa CLI Usage${C.reset}
169
+
170
+ ${C.cyan}React Power.${C.reset}
171
+ ${C.yellow}Angular Simplicity.${C.reset}
172
+ ${C.green}Vite Speed.${C.reset}
173
+ ${C.blue}Cleaner UI.${C.reset}
174
+ ${C.gray}Prebuilt structure.${C.reset}
175
+
176
+ ${C.bold}${C.blue}📌 Run generator commands from the project root folder, not from src.${C.reset}
177
+
178
+ ${C.bold}${C.cyan}Commands${C.reset}
179
+
180
+ ${C.green}nexa new app <app-name> //\\ //creates New Application${C.reset}
181
+ ${C.green}nexa new gc <name> // \\ //creates new game component${C.reset}
182
+ ${C.green}nexa new cc <name> // \\ //creates new component${C.reset}
183
+ ${C.green}nexa new svc <name> // \\ //creates new service${C.reset}
184
+ ${C.green}nexa new ctx <name> // \\//creates new context${C.reset}
185
+
186
+ ${C.bold}${C.yellow}Convenience alias also supported:${C.reset}
187
+
188
+ ${C.green}nexa new <app-name>${C.reset}
189
+
190
+ ${C.bold}${C.magenta || C.blue}Examples${C.reset}
191
+
192
+ ${C.gray}nexa new app conscious-neurons${C.reset}
193
+ ${C.gray}nexa new conscious-neurons${C.reset}
194
+ ${C.gray}nexa new gc video-card${C.reset}
195
+ ${C.gray}nexa new svc auth-service${C.reset}
196
+ ${C.gray}nexa new ctx user-session${C.reset}
197
+ `);
198
+ }
199
+ /**
200
+ * Purpose: Ask the user a yes/no question in the terminal.
201
+ */
202
+ function askYesNo(question) {
203
+ const rl = readline.createInterface({
204
+ input: process.stdin,
205
+ output: process.stdout,
206
+ });
207
+
208
+ return new Promise((resolve) => {
209
+ rl.question(question, (answer) => {
210
+ rl.close();
211
+ const normalized = answer.trim().toLowerCase();
212
+ resolve(normalized === "y" || normalized === "yes");
213
+ });
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Purpose: Open the browser to the local dev server URL.
219
+ */
220
+ function openBrowser(url) {
221
+ const platform = os.platform();
222
+
223
+ if (platform === "darwin") {
224
+ const child = spawn("open", [url], {
225
+ stdio: "ignore",
226
+ detached: true,
227
+ });
228
+ child.unref();
229
+ } else if (platform === "win32") {
230
+ const child = spawn("cmd", ["/c", "start", "", url], {
231
+ stdio: "ignore",
232
+ detached: true,
233
+ shell: true,
234
+ });
235
+ child.unref();
236
+ } else {
237
+ const child = spawn("xdg-open", [url], {
238
+ stdio: "ignore",
239
+ detached: true,
240
+ });
241
+ child.unref();
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Purpose: Start the generated app in dev mode.
247
+ */
248
+ function startGeneratedApp(projectDir, shouldOpenBrowser) {
249
+ const command = os.platform() === "win32" ? "npm.cmd" : "npm";
250
+
251
+ const devProcess = spawn(command, ["run", "dev"], {
252
+ cwd: projectDir,
253
+ stdio: "inherit",
254
+ shell: os.platform() === "win32",
255
+ });
256
+
257
+ if (shouldOpenBrowser) {
258
+ setTimeout(() => {
259
+ openBrowser("http://localhost:5725");
260
+ }, 2500);
261
+ }
262
+
263
+ devProcess.on("close", (code) => {
264
+ console.log(
265
+ `\n${C.green}✅ Dev server stopped (exit code ${code ?? 0})${C.reset}`,
266
+ );
267
+ });
268
+ }
269
+
270
+ /**
271
+ * Purpose: Create a service file in src/services.
272
+ */
273
+ function createService(serviceName) {
274
+ ensureProjectRootForGenerators();
275
+
276
+ const finalName = toPascalCase(serviceName);
277
+ const servicePath = path.join(
278
+ process.cwd(),
279
+ "src",
280
+ "services",
281
+ `${finalName}.js`,
282
+ );
283
+
284
+ const content = `/**
285
+ * File: src/services/${finalName}.js
286
+ * Purpose: Service module for ${finalName}.
287
+ * Handles API calls and external integrations.
288
+ */
289
+
290
+ const BASE_URL = "Add your url here for further use";
291
+
292
+ /**
293
+ * Example GET request
294
+ */
295
+ export async function get${finalName}() {
296
+ try {
297
+ const res = await fetch(\`\${BASE_URL}\`);
298
+ if (!res.ok) throw new Error("Failed to fetch ${finalName}");
299
+ return await res.json();
300
+ } catch (err) {
301
+ console.error("${finalName} GET error:", err);
302
+ throw err;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Example POST request
308
+ */
309
+ export async function create${finalName}(payload) {
310
+ try {
311
+ const res = await fetch(\`\${BASE_URL}\`, {
312
+ method: "POST",
313
+ headers: {
314
+ "Content-Type": "application/json",
315
+ },
316
+ body: JSON.stringify(payload),
317
+ });
318
+
319
+ if (!res.ok) throw new Error("Failed to create ${finalName}");
320
+ return await res.json();
321
+ } catch (err) {
322
+ console.error("${finalName} POST error:", err);
323
+ throw err;
324
+ }
325
+ }
326
+
327
+ export default {
328
+ get${finalName},
329
+ create${finalName},
330
+ };
331
+ `;
332
+
333
+ writeFileSafe(servicePath, content);
334
+ console.log(
335
+ `${C.green}✅ Service created at src/services/${finalName}.js${C.reset}`,
336
+ );
337
+ }
338
+
339
+ /**
340
+ * Purpose: Create a context file in src/contexts.
341
+ */
342
+ function createContext(contextName) {
343
+ ensureProjectRootForGenerators();
344
+
345
+ const finalName = toPascalCase(contextName);
346
+ const ctxPath = path.join(
347
+ process.cwd(),
348
+ "src",
349
+ "contexts",
350
+ `${finalName}.js`,
351
+ );
352
+
353
+ const content = `/**
354
+ * File: src/contexts/${finalName}.js
355
+ * Purpose: React context definition for ${finalName}.
356
+ */
357
+
358
+ import { createContext } from "react";
359
+
360
+ export const ${finalName} = createContext(null);
361
+ `;
362
+
363
+ writeFileSafe(ctxPath, content);
364
+ console.log(
365
+ `${C.green}✅ Context created at src/contexts/${finalName}.js${C.reset}`,
366
+ );
367
+ }
368
+
369
+ /**
370
+ * Purpose: Create a component folder with JSX, child JSX, and CSS.
371
+ */
372
+
373
+ function createComponent(componentName) {
374
+ ensureProjectRootForGenerators();
375
+
376
+ const finalName = toPascalCase(componentName);
377
+ const childName = `${finalName}JS`;
378
+ const kebabName = toKebabCase(componentName);
379
+ const routePath = `/${kebabName}`;
380
+
381
+ const componentDir = path.join(process.cwd(), "src", "components", finalName);
382
+ const routeMetaPath = path.join(
383
+ process.cwd(),
384
+ "src",
385
+ "config",
386
+ "routeMeta.js",
387
+ );
388
+
389
+ const jsxContent = `import React from "react";
390
+ import logo from "../../assets/nexa.svg";
391
+ import "./${finalName}.css";
392
+ import ${childName} from "./${childName}";
393
+
394
+ const ${finalName} = () => {
395
+ return (
396
+ <section className="home-page">
397
+ <div className="nexa-intro">
398
+ <div className="nexa-logo-wrap">
399
+ <div className="nexa-logo-convex" />
400
+ <div className="nexa-logo-shine" />
401
+ <img src={logo} alt="Nexa Logo" className="nexa-logo-img" />
402
+ </div>
403
+
404
+ <h2 className="nexa-wordmark">${finalName}</h2>
405
+ <p className="nexa-tagline">Cleaner UI. Prebuilt structure.</p>
406
+ <p className="nexa-credit">
407
+ A product of <span>Conscious Neurons</span>
408
+ </p>
409
+
410
+ <p className="${kebabName}-note">
411
+ This component was created by Nexa.
412
+ </p>
413
+
414
+ <div className="${kebabName}-child">
415
+ <${childName} />
416
+ </div>
417
+ </div>
418
+ </section>
419
+ );
420
+ };
421
+
422
+ export default ${finalName};
423
+ `;
424
+
425
+ const childContent = `import React from "react";
426
+
427
+ const ${childName} = () => {
428
+ return (
429
+ <div className="${kebabName}-child-inner">
430
+ <p>This is the ${childName} child component.</p>
431
+ </div>
432
+ );
433
+ };
434
+
435
+ export default ${childName};
436
+ `;
437
+
438
+ const cssContent = `.home-page {
439
+ min-height: calc(100vh - 120px);
440
+ display: flex;
441
+ align-items: center;
442
+ justify-content: center;
443
+ padding: 24px;
444
+ }
445
+
446
+ .nexa-intro {
447
+ text-align: center;
448
+ display: flex;
449
+ flex-direction: column;
450
+ align-items: center;
451
+ justify-content: center;
452
+ }
453
+
454
+ .nexa-logo-wrap {
455
+ position: relative;
456
+ width: 190px;
457
+ height: 190px;
458
+ display: grid;
459
+ place-items: center;
460
+ margin-bottom: 20px;
461
+ border-radius: 40px;
462
+ background: radial-gradient(
463
+ circle at center,
464
+ rgba(62, 231, 255, 0.1),
465
+ rgba(255, 255, 255, 0.02)
466
+ );
467
+ box-shadow:
468
+ 0 0 60px rgba(62, 231, 255, 0.1),
469
+ inset 0 0 30px rgba(255, 255, 255, 0.025);
470
+ overflow: hidden;
471
+ }
472
+
473
+ .nexa-logo-convex {
474
+ position: absolute;
475
+ inset: 10%;
476
+ border-radius: 28px;
477
+ background: radial-gradient(
478
+ circle at 35% 28%,
479
+ rgba(255, 255, 255, 0.16),
480
+ rgba(255, 255, 255, 0.04) 28%,
481
+ rgba(255, 255, 255, 0.01) 52%,
482
+ rgba(0, 0, 0, 0.04) 100%
483
+ );
484
+ box-shadow:
485
+ inset 0 2px 10px rgba(255, 255, 255, 0.06),
486
+ inset 0 -10px 18px rgba(0, 0, 0, 0.12);
487
+ pointer-events: none;
488
+ }
489
+
490
+ .nexa-logo-img {
491
+ width: 112px;
492
+ height: 112px;
493
+ object-fit: contain;
494
+ position: relative;
495
+ z-index: 1;
496
+ transform: scale(0.2);
497
+ opacity: 0;
498
+ filter: blur(14px);
499
+ animation: nexaReveal 1.2s ease-out forwards;
500
+ }
501
+
502
+ .nexa-logo-shine {
503
+ position: absolute;
504
+ inset: -40%;
505
+ background: linear-gradient(
506
+ 110deg,
507
+ transparent 35%,
508
+ rgba(255, 255, 255, 0.04) 45%,
509
+ rgba(255, 255, 255, 0.35) 50%,
510
+ rgba(255, 255, 255, 0.04) 55%,
511
+ transparent 65%
512
+ );
513
+ transform: translateX(-140%) rotate(12deg);
514
+ animation: nexaShine 1.6s ease 0.65s forwards;
515
+ pointer-events: none;
516
+ }
517
+
518
+ .nexa-wordmark {
519
+ margin: 0 0 8px;
520
+ font-size: clamp(2.2rem, 5vw, 3.8rem);
521
+ font-weight: 700;
522
+ letter-spacing: -0.04em;
523
+ color: var(--nexa-text);
524
+ opacity: 0;
525
+ transform: translateY(10px);
526
+ animation: fadeUp 0.7s ease 0.8s forwards;
527
+ }
528
+
529
+ .nexa-tagline {
530
+ margin: 0;
531
+ font-size: 1rem;
532
+ color: var(--nexa-text-dim);
533
+ letter-spacing: 0.02em;
534
+ opacity: 0;
535
+ transform: translateY(10px);
536
+ animation: fadeUp 0.7s ease 1s forwards;
537
+ }
538
+
539
+ .nexa-credit {
540
+ margin-top: 10px;
541
+ font-size: 0.82rem;
542
+ color: var(--nexa-text-dim);
543
+ opacity: 0;
544
+ transform: translateY(10px);
545
+ animation: fadeUp 0.7s ease 1.2s forwards;
546
+ }
547
+
548
+ .nexa-credit span {
549
+ color: var(--nexa-accent);
550
+ font-weight: 600;
551
+ }
552
+
553
+ .${kebabName}-note {
554
+ margin-top: 10px;
555
+ font-size: 0.9rem;
556
+ color: var(--nexa-text-dim);
557
+ opacity: 0;
558
+ transform: translateY(10px);
559
+ animation: fadeUp 0.7s ease 1.35s forwards;
560
+ }
561
+
562
+ .${kebabName}-child {
563
+ margin-top: 14px;
564
+ opacity: 0;
565
+ transform: translateY(10px);
566
+ animation: fadeUp 0.7s ease 1.5s forwards;
567
+ }
568
+
569
+ .${kebabName}-child-inner {
570
+ padding: 0;
571
+ margin: 0;
572
+ background: transparent;
573
+ border: none;
574
+ box-shadow: none;
575
+ }
576
+
577
+ .${kebabName}-child-inner p {
578
+ margin: 0;
579
+ color: var(--nexa-text-dim);
580
+ }
581
+
582
+ @keyframes nexaReveal {
583
+ 0% {
584
+ transform: scale(0.2);
585
+ opacity: 0;
586
+ filter: blur(14px);
587
+ }
588
+ 55% {
589
+ transform: scale(1.08);
590
+ opacity: 1;
591
+ filter: blur(0);
592
+ }
593
+ 100% {
594
+ transform: scale(1);
595
+ opacity: 1;
596
+ filter: blur(0);
597
+ }
598
+ }
599
+
600
+ @keyframes nexaShine {
601
+ 0% {
602
+ transform: translateX(-140%) rotate(12deg);
603
+ opacity: 0;
604
+ }
605
+ 20% {
606
+ opacity: 1;
607
+ }
608
+ 100% {
609
+ transform: translateX(140%) rotate(12deg);
610
+ opacity: 0;
611
+ }
612
+ }
613
+
614
+ @keyframes fadeUp {
615
+ to {
616
+ opacity: 1;
617
+ transform: translateY(0);
618
+ }
619
+ }
620
+
621
+ @media (max-width: 768px) {
622
+ .home-page {
623
+ min-height: calc(100vh - 100px);
624
+ padding: 16px;
625
+ }
626
+
627
+ .nexa-logo-wrap {
628
+ width: 160px;
629
+ height: 160px;
630
+ border-radius: 32px;
631
+ }
632
+
633
+ .nexa-logo-convex {
634
+ border-radius: 22px;
635
+ }
636
+
637
+ .nexa-logo-img {
638
+ width: 92px;
639
+ height: 92px;
640
+ }
641
+
642
+ .nexa-wordmark {
643
+ font-size: 2.3rem;
644
+ }
645
+
646
+ .nexa-tagline {
647
+ font-size: 0.92rem;
648
+ }
649
+ }
650
+ `;
651
+
652
+ writeFileSafe(path.join(componentDir, `${finalName}.jsx`), jsxContent);
653
+ writeFileSafe(path.join(componentDir, `${childName}.jsx`), childContent);
654
+ writeFileSafe(path.join(componentDir, `${finalName}.css`), cssContent);
655
+
656
+ if (fs.existsSync(routeMetaPath)) {
657
+ let routeMetaContent = fs.readFileSync(routeMetaPath, "utf8");
658
+
659
+ if (!routeMetaContent.includes(`"${routePath}"`)) {
660
+ const entry = ` "${routePath}": {
661
+ navLabel: "${finalName}",
662
+ title: "${finalName}",
663
+ subtitle: "Generated instantly by Nexa CLI",
664
+ tooltip: "This page was auto-generated by the Nexa CLI",
665
+ showInNav: true,
666
+ }`;
667
+
668
+ routeMetaContent = routeMetaContent.replace(
669
+ /(\s*)};\s*$/,
670
+ `,\n${entry}\n};`,
671
+ );
672
+
673
+ routeMetaContent = routeMetaContent.replace(/,\s*,/g, ",");
674
+
675
+ fs.writeFileSync(routeMetaPath, routeMetaContent, "utf8");
676
+ console.log(
677
+ `${C.blue}ℹ Added routeMeta entry for ${routePath}${C.reset}`,
678
+ );
679
+ }
680
+ } else {
681
+ console.log(
682
+ `${C.yellow}⚠ routeMeta.js not found. Component created, but route was not added.${C.reset}`,
683
+ );
684
+ }
685
+
686
+ const importPath = `./components/${finalName}/${finalName}`;
687
+
688
+ console.log(`
689
+ ${C.cyan}📌 Next Step:${C.reset}
690
+
691
+ ${C.yellow}1. Import your component in App.jsx:${C.reset}
692
+ ${C.gray}import ${finalName} from "${importPath}";${C.reset}
693
+
694
+ ${C.yellow}2. Add the route inside <Routes>:${C.reset}
695
+ ${C.gray}<Route path="${routePath}" element={<${finalName} />} />${C.reset}
696
+ `);
697
+
698
+ console.log(
699
+ `${C.green}✅ Component '${finalName}' created at src/components/${finalName}${C.reset}`,
700
+ );
701
+ }
702
+ /**
703
+ * Purpose: Create a full app scaffold from the template folder and then
704
+ * patch key files with the correct app-specific values.
705
+ */
706
+ async function createApp(rawAppName) {
707
+ const projectDirName = toKebabCase(rawAppName);
708
+ const displayName = toPascalCase(rawAppName);
709
+ const packageName = toKebabCase(rawAppName);
710
+
711
+ const root = process.cwd();
712
+ const projectDir = path.join(root, projectDirName);
713
+ const templateDir = path.join(__dirname, "../template");
714
+
715
+ if (!rawAppName) {
716
+ console.error(`${C.yellow}❌ Please provide an app name.${C.reset}`);
717
+ console.error(`${C.green}Example: nexa new app canna-core-420${C.reset}`);
718
+ process.exit(1);
719
+ }
720
+
721
+ if (!fs.existsSync(templateDir)) {
722
+ console.error(`${C.yellow}❌ Template directory not found.${C.reset}`);
723
+ console.error(`${C.gray}Expected template at: ${templateDir}${C.reset}`);
724
+ process.exit(1);
725
+ }
726
+
727
+ if (fs.existsSync(projectDir)) {
728
+ console.error(
729
+ `${C.yellow}❌ Folder already exists: ${projectDirName}${C.reset}`,
730
+ );
731
+ process.exit(1);
732
+ }
733
+
734
+ fs.mkdirSync(projectDir, { recursive: true });
735
+
736
+ copyRecursive(templateDir, projectDir);
737
+
738
+ const gitignorePath = path.join(projectDir, ".gitignore");
739
+
740
+ if (!fs.existsSync(gitignorePath)) {
741
+ const gitignoreContent = `# Dependencies
742
+ node_modules/
743
+ npm-debug.log*
744
+ yarn-debug.log*
745
+ yarn-error.log*
746
+
747
+ # Build output
748
+ dist/
749
+ build/
750
+
751
+ # Environment variables
752
+ .env
753
+ .env.*
754
+ !.env.example
755
+
756
+ # Logs
757
+ logs/
758
+ *.log
759
+
760
+ # OS files
761
+ .DS_Store
762
+ Thumbs.db
763
+
764
+ # Editor / IDE
765
+ .vscode/
766
+ .idea/
767
+ *.suo
768
+ *.ntvs*
769
+ *.njsproj
770
+ *.sln
771
+
772
+ # Temporary files
773
+ tmp/
774
+ temp/
775
+ *.tmp
776
+
777
+ # Coverage
778
+ coverage/
779
+
780
+ # Cache
781
+ .cache/
782
+ .parcel-cache/
783
+ .vite/
784
+
785
+ # Optional: lock files (keep if you want reproducible builds)
786
+ # package-lock.json
787
+ # yarn.lock
788
+
789
+ # Misc
790
+ *.tgz
791
+ `;
792
+ writeFileSafe(gitignorePath, gitignoreContent);
793
+ }
794
+
795
+ const publicDir = path.join(projectDir, "public");
796
+ const srcDir = path.join(projectDir, "src");
797
+
798
+ fs.mkdirSync(publicDir, { recursive: true });
799
+ fs.mkdirSync(srcDir, { recursive: true });
800
+
801
+ const publicIndexPath = path.join(publicDir, "index.html");
802
+ const rootIndexPath = path.join(projectDir, "index.html");
803
+
804
+ if (fs.existsSync(publicIndexPath)) {
805
+ const templateIndex = fs.readFileSync(publicIndexPath, "utf8");
806
+ const patchedIndex = templateIndex
807
+ .replace(/<title>.*?<\/title>/i, `<title>Nexa • ${displayName}</title>`)
808
+ .replace(/src=["']\.\/src\/main\.jsx["']/i, 'src="/src/main.jsx"')
809
+ .replace(/src=["']src\/main\.jsx["']/i, 'src="/src/main.jsx"');
810
+
811
+ writeFileSafe(rootIndexPath, patchedIndex);
812
+ fs.unlinkSync(publicIndexPath);
813
+ } else if (!fs.existsSync(rootIndexPath)) {
814
+ const indexHtmlContent = `<!DOCTYPE html>
815
+ <html lang="en">
816
+ <head>
817
+ <!-- File: index.html -->
818
+ <!-- Purpose: Root HTML document for the generated Vite application. -->
819
+ <meta charset="UTF-8" />
820
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
821
+
822
+ <!-- PWA -->
823
+ <meta name="theme-color" content="#3ee7ff" />
824
+ <link rel="manifest" href="/manifest.json" />
825
+
826
+ <!-- Icons -->
827
+ <link rel="icon" type="image/png" href="/favicon.png" />
828
+
829
+ <!-- Title -->
830
+ <title>Nexa • ${displayName}</title>
831
+
832
+ </head>
833
+ <body>
834
+ <div id="root"></div>
835
+ <script type="module" src="/src/main.jsx"></script>
836
+ </body>
837
+ </html>
838
+ `;
839
+ writeFileSafe(rootIndexPath, indexHtmlContent);
840
+ }
841
+
842
+ const manifestPath = path.join(publicDir, "manifest.json");
843
+ const manifestContent = `{
844
+ "name": "Nexa App",
845
+ "short_name": "Nexa",
846
+ "start_url": "/",
847
+ "display": "standalone",
848
+ "background_color": "#07111f",
849
+ "theme_color": "#3ee7ff",
850
+ "icons": [
851
+ {
852
+ "src": "/icons/icon-192.png",
853
+ "sizes": "192x192",
854
+ "type": "image/png",
855
+ "purpose": "any maskable"
856
+ },
857
+ {
858
+ "src": "/icons/icon-512.png",
859
+ "sizes": "512x512",
860
+ "type": "image/png",
861
+ "purpose": "any maskable"
862
+ },
863
+ {
864
+ "src": "/nexa.svg",
865
+ "sizes": "any",
866
+ "type": "image/svg+xml",
867
+ "purpose": "any"
868
+ }
869
+ ]
870
+ }
871
+ `;
872
+ writeFileSafe(manifestPath, manifestContent);
873
+
874
+ const generatedPkg = {
875
+ name: packageName,
876
+ version: "1.0.0",
877
+ private: true,
878
+ type: "module",
879
+ scripts: {
880
+ dev: "node run.js",
881
+ start: "node run.js",
882
+ nexa: "node run.js",
883
+ build: "vite build --config nexa.config.js",
884
+ preview: "vite preview --config nexa.config.js",
885
+ },
886
+ dependencies: {
887
+ react: "^18.3.1",
888
+ "react-dom": "^18.3.1",
889
+ "react-router-dom": "^6.15.0",
890
+ },
891
+ devDependencies: {
892
+ vite: "^7.2.7",
893
+ "@vitejs/plugin-react": "^4.3.3",
894
+ },
895
+ };
896
+
897
+ writeFileSafe(
898
+ path.join(projectDir, "package.json"),
899
+ `${JSON.stringify(generatedPkg, null, 2)}\n`,
900
+ );
901
+
902
+ const mainJsxPath = path.join(srcDir, "main.jsx");
903
+ if (!fs.existsSync(mainJsxPath)) {
904
+ const mainJsxContent = `/**
905
+ * File: src/main.jsx
906
+ * Purpose: Entry point for the React application. Mounts App into the root DOM node.
907
+ */
908
+
909
+ import React from "react";
910
+ import ReactDOM from "react-dom/client";
911
+ import App from "./App.jsx";
912
+ import "./index.css";
913
+
914
+ ReactDOM.createRoot(document.getElementById("root")).render(
915
+ <React.StrictMode>
916
+ <App />
917
+ </React.StrictMode>
918
+ );
919
+ `;
920
+ writeFileSafe(mainJsxPath, mainJsxContent);
921
+ }
922
+
923
+ const appJsxPath = path.join(srcDir, "App.jsx");
924
+ if (!fs.existsSync(appJsxPath)) {
925
+ const appJsxContent = `/**
926
+ * File: src/App.jsx
927
+ * Purpose: Main root component for the generated Nexa application.
928
+ */
929
+
930
+ import React from "react";
931
+ import "./App.css";
932
+
933
+ const App = () => {
934
+ return (
935
+ <div className="app-container">
936
+ <h1>Welcome to ${displayName}</h1>
937
+ <p>Your Nexa app is ready.</p>
938
+ <p>React Power. Angular Simplicity. Vite Speed.</p>
939
+ <p>Cleaner UI. Prebuilt structure.</p>
940
+ <p>
941
+ Powered by{" "}
942
+ <a
943
+ href="https://consciousneurons.com"
944
+ target="_blank"
945
+ rel="noopener noreferrer"
946
+ >
947
+ Conscious Neurons LLC
948
+ </a>
949
+ {" "} | Sponsored by{" "}
950
+ <a
951
+ href="https://albagoldsystems.com"
952
+ target="_blank"
953
+ rel="noopener noreferrer"
954
+ >
955
+ Alba Gold
956
+ </a>
957
+ </p>
958
+ </div>
959
+ );
960
+ };
961
+
962
+ export default App;
963
+ `;
964
+ writeFileSafe(appJsxPath, appJsxContent);
965
+ }
966
+
967
+ const appCssPath = path.join(srcDir, "App.css");
968
+ if (!fs.existsSync(appCssPath)) {
969
+ const appCssContent = `/**
970
+ * File: src/App.css
971
+ * Purpose: Base styling for the generated Nexa application shell.
972
+ */
973
+
974
+ body {
975
+ margin: 0;
976
+ font-family: Inter, sans-serif;
977
+ background-color: #0a0f24;
978
+ color: #f8f9fc;
979
+ }
980
+
981
+ a {
982
+ color: #ffd700;
983
+ text-decoration: none;
984
+ }
985
+
986
+ a:hover {
987
+ text-decoration: underline;
988
+ }
989
+
990
+ .app-container {
991
+ display: flex;
992
+ min-height: 100vh;
993
+ padding: 32px;
994
+ flex-direction: column;
995
+ align-items: center;
996
+ justify-content: center;
997
+ text-align: center;
998
+ }
999
+ `;
1000
+ writeFileSafe(appCssPath, appCssContent);
1001
+ }
1002
+
1003
+ const runJsPath = path.join(projectDir, "run.js");
1004
+ if (!fs.existsSync(runJsPath)) {
1005
+ const runJsContent = `#!/usr/bin/env node
1006
+
1007
+ /**
1008
+ * File: run.js
1009
+ * Purpose: Launches the Nexa app using Vite with custom config.
1010
+ */
1011
+
1012
+ import { spawn } from "child_process";
1013
+ import path from "path";
1014
+ import os from "os";
1015
+ import fs from "fs";
1016
+
1017
+ const C = {
1018
+ reset: "\\x1b[0m",
1019
+ cyan: "\\x1b[36m",
1020
+ yellow: "\\x1b[33m",
1021
+ green: "\\x1b[32m",
1022
+ blue: "\\x1b[34m",
1023
+ gray: "\\x1b[90m",
1024
+ bold: "\\x1b[1m",
1025
+ };
1026
+
1027
+ console.log(\`
1028
+ \${C.cyan}\${C.bold}🚀 Nexa CLI\${C.reset} - Powered by Conscious Neurons LLC
1029
+ \${C.gray}https://consciousneurons.com\${C.reset}
1030
+ Built by Salman Saeed
1031
+
1032
+ \${C.yellow}Cleaner UI. Prebuilt structure.\${C.reset}
1033
+ \${C.gray}Everything important is already in place.\${C.reset}
1034
+
1035
+ \${C.green}🔹 Starting your Nexa App...\${C.reset}
1036
+ \${C.cyan}🔹 React Power.\${C.reset}
1037
+ \${C.yellow}🔹 Angular Simplicity.\${C.reset}
1038
+ \${C.blue}🔹 Vite Speed.\${C.reset}
1039
+ \${C.green}🔹 Cleaner UI.\${C.reset}
1040
+ \${C.gray}🔹 Prebuilt structure.${C.reset}
1041
+
1042
+ \${C.cyan}███╗ ██╗███████╗██╗ ██╗ █████╗\${C.reset}
1043
+ \${C.cyan}████╗ ██║██╔════╝╚██╗██╔╝██╔══██╗\${C.reset}
1044
+ \${C.cyan}██╔██╗ ██║█████╗ ╚███╔╝ ███████║\${C.reset}
1045
+ \${C.cyan}██║╚██╗██║██╔══╝ ██╔██╗ ██╔══██║\${C.reset}
1046
+ \${C.cyan}██║ ╚████║███████╗██╔╝ ██╗██║ ██║\${C.reset}
1047
+ \${C.cyan}╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝\${C.reset}
1048
+
1049
+ \${C.gray}by https://salmansaeed.us\${C.reset}
1050
+ \`);
1051
+
1052
+ const configPath = path.resolve("./nexa.config.js");
1053
+ const hasConfig = fs.existsSync(configPath);
1054
+
1055
+ const command = os.platform() === "win32" ? "npx.cmd" : "npx";
1056
+ const viteArgs = ["vite"];
1057
+
1058
+ if (hasConfig) {
1059
+ viteArgs.push("--config", configPath);
1060
+ }
1061
+
1062
+ const vite = spawn(command, viteArgs, {
1063
+ stdio: "pipe",
1064
+ shell: os.platform() === "win32",
1065
+ });
1066
+
1067
+ vite.stdout.on("data", (data) => {
1068
+ const str = data.toString();
1069
+ if (!str.includes("VITE")) {
1070
+ console.log(str);
1071
+ }
1072
+ });
1073
+
1074
+ vite.stderr.on("data", (data) => {
1075
+ process.stderr.write(data);
1076
+ });
1077
+
1078
+ vite.on("close", (code) => {
1079
+ console.log(\`\\n\${C.green}✅ Nexa App stopped (exit code \${code ?? 0})\${C.reset}\`);
1080
+ process.exit(code ?? 0);
1081
+ });
1082
+ `;
1083
+ writeFileSafe(runJsPath, runJsContent);
1084
+ }
1085
+
1086
+ const nexaConfigPath = path.join(projectDir, "nexa.config.js");
1087
+ if (!fs.existsSync(nexaConfigPath)) {
1088
+ const nexaConfigContent = `/**
1089
+ * File: nexa.config.js
1090
+ * Purpose: Vite configuration for Nexa-generated apps.
1091
+ */
1092
+
1093
+ import { defineConfig } from "vite";
1094
+ import react from "@vitejs/plugin-react";
1095
+
1096
+ export default defineConfig({
1097
+ root: ".",
1098
+ base: "/",
1099
+ plugins: [react()],
1100
+ server: {
1101
+ port: 5725,
1102
+ },
1103
+ build: {
1104
+ outDir: "dist",
1105
+ },
1106
+ clearScreen: false,
1107
+ });
1108
+ `;
1109
+ writeFileSafe(nexaConfigPath, nexaConfigContent);
1110
+ }
1111
+
1112
+ console.log(`\n${C.blue}📦 Installing dependencies...${C.reset}`);
1113
+ let installSucceeded = false;
1114
+
1115
+ try {
1116
+ execSync("npm install", { stdio: "inherit", cwd: projectDir });
1117
+ installSucceeded = true;
1118
+ console.log(`${C.green}✅ Dependencies installed successfully!${C.reset}`);
1119
+ } catch {
1120
+ console.error(
1121
+ `${C.yellow}❌ Failed to install dependencies. Run 'npm install' manually.${C.reset}`,
1122
+ );
1123
+ }
1124
+
1125
+ console.log(`\n${C.green}🎉 Project created successfully!${C.reset}`);
1126
+ console.log(`${C.cyan}React Power.${C.reset}`);
1127
+ console.log(`${C.yellow}Angular Simplicity.${C.reset}`);
1128
+ console.log(`${C.green}Vite Speed.${C.reset}`);
1129
+ console.log(`${C.blue}Cleaner UI.${C.reset}`);
1130
+ console.log(`${C.gray}Prebuilt structure.${C.reset}`);
1131
+ console.log(`${C.gray}cd ${projectDirName}${C.reset}`);
1132
+ console.log(`${C.gray}npm run dev${C.reset}`);
1133
+ console.log(`${C.gray}npm run build${C.reset}`);
1134
+ console.log(`${C.gray}npm run preview${C.reset}`);
1135
+
1136
+ if (installSucceeded) {
1137
+ const shouldStart = await askYesNo(
1138
+ `\n${C.green}🚀 Auto start the app now? (y/n): ${C.reset}`,
1139
+ );
1140
+
1141
+ if (shouldStart) {
1142
+ const shouldOpen = await askYesNo(
1143
+ `${C.blue}🌐 Open in browser automatically? (y/n): ${C.reset}`,
1144
+ );
1145
+
1146
+ console.log(
1147
+ `\n${C.green}▶️ Starting app in ${projectDirName}...${C.reset}\n`,
1148
+ );
1149
+ startGeneratedApp(projectDir, shouldOpen);
1150
+ }
1151
+ }
1152
+ }
1153
+
1154
+ /**
1155
+ * Purpose: Parse command arguments and support:
1156
+ * nexa new app my-app
1157
+ * nexa new my-app
1158
+ * nexa app my-app
1159
+ */
1160
+ function parseArgs(argv) {
1161
+ const first = argv[0];
1162
+ const second = argv[1];
1163
+ const third = argv[2];
1164
+
1165
+ if (!first) {
1166
+ printUsage();
1167
+ process.exit(1);
1168
+ }
1169
+
1170
+ if (first === "help" || first === "--help" || first === "-h") {
1171
+ printUsage();
1172
+ process.exit(0);
1173
+ }
1174
+
1175
+ if (first === "new" && ["app", "gc", "cc", "svc", "ctx"].includes(second)) {
1176
+ return {
1177
+ shortcut: second,
1178
+ name: third,
1179
+ };
1180
+ }
1181
+
1182
+ if (
1183
+ first === "new" &&
1184
+ second &&
1185
+ !["app", "gc", "cc", "svc", "ctx"].includes(second)
1186
+ ) {
1187
+ return {
1188
+ shortcut: "app",
1189
+ name: second,
1190
+ };
1191
+ }
1192
+
1193
+ if (["app", "gc", "cc", "svc", "ctx"].includes(first)) {
1194
+ return {
1195
+ shortcut: first,
1196
+ name: second,
1197
+ };
1198
+ }
1199
+
1200
+ printUsage();
1201
+ process.exit(1);
1202
+ }
1203
+
1204
+ const { shortcut, name } = parseArgs(args);
1205
+
1206
+ async function main() {
1207
+ switch (shortcut) {
1208
+ case "svc":
1209
+ if (!name) {
1210
+ console.error(`${C.yellow}❌ Please provide a service name.${C.reset}`);
1211
+ process.exit(1);
1212
+ }
1213
+ createService(name);
1214
+ break;
1215
+
1216
+ case "ctx":
1217
+ if (!name) {
1218
+ console.error(`${C.yellow}❌ Please provide a context name.${C.reset}`);
1219
+ process.exit(1);
1220
+ }
1221
+ createContext(name);
1222
+ break;
1223
+
1224
+ case "gc":
1225
+ case "cc":
1226
+ if (!name) {
1227
+ console.error(
1228
+ `${C.yellow}❌ Please provide a component name.${C.reset}`,
1229
+ );
1230
+ process.exit(1);
1231
+ }
1232
+ createComponent(name);
1233
+ break;
1234
+
1235
+ case "app":
1236
+ if (!name) {
1237
+ console.error(`${C.yellow}❌ Please provide an app name.${C.reset}`);
1238
+ process.exit(1);
1239
+ }
1240
+ await createApp(name);
1241
+ break;
1242
+
1243
+ default:
1244
+ console.error(`${C.yellow}❌ Unknown shortcut.${C.reset}`);
1245
+ printUsage();
1246
+ process.exit(1);
1247
+ }
1248
+ }
1249
+
1250
+ main().catch((err) => {
1251
+ console.error(`${C.yellow}❌ Unexpected error:${C.reset}`, err);
1252
+ process.exit(1);
1253
+ });