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