@zenithbuild/cli 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,702 @@
1
+ /**
2
+ * @zenithbuild/cli - Create Command
3
+ *
4
+ * Scaffolds a new Zenith application with interactive prompts,
5
+ * branded visuals, and optional configuration generation.
6
+ */
7
+
8
+ import fs from 'fs'
9
+ import path from 'path'
10
+ import { execSync } from 'child_process'
11
+ import readline from 'readline'
12
+ import * as brand from '../utils/branding'
13
+
14
+ // Types for project options
15
+ interface ProjectOptions {
16
+ name: string
17
+ directory: 'app' | 'src'
18
+ eslint: boolean
19
+ prettier: boolean
20
+ pathAlias: boolean
21
+ }
22
+
23
+ /**
24
+ * Interactive readline prompt helper
25
+ */
26
+ async function prompt(question: string, defaultValue?: string): Promise<string> {
27
+ const rl = readline.createInterface({
28
+ input: process.stdin,
29
+ output: process.stdout
30
+ })
31
+
32
+ const displayQuestion = defaultValue
33
+ ? `${question} ${brand.dim(`(${defaultValue})`)}: `
34
+ : `${question}: `
35
+
36
+ return new Promise((resolve) => {
37
+ rl.question(displayQuestion, (answer) => {
38
+ rl.close()
39
+ resolve(answer.trim() || defaultValue || '')
40
+ })
41
+ })
42
+ }
43
+
44
+ /**
45
+ * Yes/No prompt helper
46
+ */
47
+ async function confirm(question: string, defaultYes: boolean = true): Promise<boolean> {
48
+ const hint = defaultYes ? 'Y/n' : 'y/N'
49
+ const answer = await prompt(`${question} ${brand.dim(`(${hint})`)}`)
50
+
51
+ if (!answer) return defaultYes
52
+ return answer.toLowerCase().startsWith('y')
53
+ }
54
+
55
+ /**
56
+ * Main create command
57
+ */
58
+ export async function create(appName?: string): Promise<void> {
59
+ // Show branded intro
60
+ await brand.showIntro()
61
+ brand.header('Create a new Zenith app')
62
+
63
+ // Gather project options
64
+ const options = await gatherOptions(appName)
65
+
66
+ console.log('')
67
+ const spinner = new brand.Spinner('Creating project structure...')
68
+ spinner.start()
69
+
70
+ try {
71
+ // Create project
72
+ await createProject(options)
73
+ spinner.succeed('Project structure created')
74
+
75
+ // Always generate configs (tsconfig.json is required)
76
+ const configSpinner = new brand.Spinner('Generating configurations...')
77
+ configSpinner.start()
78
+ await generateConfigs(options)
79
+ configSpinner.succeed('Configurations generated')
80
+
81
+ // Install dependencies
82
+ const installSpinner = new brand.Spinner('Installing dependencies...')
83
+ installSpinner.start()
84
+
85
+ const targetDir = path.resolve(process.cwd(), options.name)
86
+ process.chdir(targetDir)
87
+
88
+ try {
89
+ execSync('bun install', { stdio: 'pipe' })
90
+ installSpinner.succeed('Dependencies installed')
91
+ } catch {
92
+ try {
93
+ execSync('npm install', { stdio: 'pipe' })
94
+ installSpinner.succeed('Dependencies installed')
95
+ } catch {
96
+ installSpinner.fail('Could not install dependencies automatically')
97
+ brand.warn('Run "bun install" or "npm install" manually')
98
+ }
99
+ }
100
+
101
+ // Show success message
102
+ brand.showNextSteps(options.name)
103
+
104
+ } catch (err: unknown) {
105
+ spinner.fail('Failed to create project')
106
+ const message = err instanceof Error ? err.message : String(err)
107
+ brand.error(message)
108
+ process.exit(1)
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Gather all project options through interactive prompts
114
+ */
115
+ async function gatherOptions(providedName?: string): Promise<ProjectOptions> {
116
+ // Project name
117
+ let name = providedName
118
+ if (!name) {
119
+ name = await prompt(brand.highlight('Project name'))
120
+ if (!name) {
121
+ brand.error('Project name is required')
122
+ process.exit(1)
123
+ }
124
+ }
125
+
126
+ const targetDir = path.resolve(process.cwd(), name)
127
+ if (fs.existsSync(targetDir)) {
128
+ brand.error(`Directory "${name}" already exists`)
129
+ process.exit(1)
130
+ }
131
+
132
+ console.log('')
133
+ brand.info(`Creating ${brand.bold(name)} in ${brand.dim(targetDir)}`)
134
+ console.log('')
135
+
136
+ // Directory structure
137
+ const useSrc = await confirm('Use src/ directory instead of app/?', false)
138
+ const directory = useSrc ? 'src' : 'app'
139
+
140
+ // ESLint
141
+ const eslint = await confirm('Add ESLint for code linting?', true)
142
+
143
+ // Prettier
144
+ const prettier = await confirm('Add Prettier for code formatting?', true)
145
+
146
+ // TypeScript path aliases
147
+ const pathAlias = await confirm('Add TypeScript path alias (@/*)?', true)
148
+
149
+ return {
150
+ name,
151
+ directory,
152
+ eslint,
153
+ prettier,
154
+ pathAlias
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Create the project directory structure and files
160
+ */
161
+ async function createProject(options: ProjectOptions): Promise<void> {
162
+ const targetDir = path.resolve(process.cwd(), options.name)
163
+ const baseDir = options.directory
164
+ const appDir = path.join(targetDir, baseDir)
165
+
166
+ // Create directories
167
+ fs.mkdirSync(targetDir, { recursive: true })
168
+ fs.mkdirSync(path.join(appDir, 'pages'), { recursive: true })
169
+ fs.mkdirSync(path.join(appDir, 'layouts'), { recursive: true })
170
+ fs.mkdirSync(path.join(appDir, 'components'), { recursive: true })
171
+ fs.mkdirSync(path.join(appDir, 'styles'), { recursive: true }) // Create styles inside appDir
172
+
173
+ // package.json
174
+ const pkg: Record<string, unknown> = {
175
+ name: options.name,
176
+ version: '0.1.0',
177
+ private: true,
178
+ type: 'module',
179
+ scripts: {
180
+ dev: 'zen-dev',
181
+ build: 'zen-build',
182
+ preview: 'zen-preview',
183
+ test: 'bun test'
184
+ },
185
+ dependencies: {
186
+ '@zenithbuild/core': '^0.1.0'
187
+ },
188
+ devDependencies: {
189
+ '@types/bun': 'latest'
190
+ } as Record<string, string>
191
+ }
192
+
193
+ // Add optional dev dependencies
194
+ const devDeps = pkg.devDependencies as Record<string, string>
195
+ if (options.eslint) {
196
+ devDeps['eslint'] = '^8.0.0'
197
+ devDeps['@typescript-eslint/eslint-plugin'] = '^6.0.0'
198
+ devDeps['@typescript-eslint/parser'] = '^6.0.0'
199
+ pkg.scripts = { ...(pkg.scripts as object), lint: 'eslint .' }
200
+ }
201
+ if (options.prettier) {
202
+ devDeps['prettier'] = '^3.0.0'
203
+ pkg.scripts = { ...(pkg.scripts as object), format: 'prettier --write .' }
204
+ }
205
+
206
+ fs.writeFileSync(
207
+ path.join(targetDir, 'package.json'),
208
+ JSON.stringify(pkg, null, 4)
209
+ )
210
+
211
+ // index.zen
212
+ fs.writeFileSync(
213
+ path.join(targetDir, baseDir, 'pages', 'index.zen'),
214
+ generateIndexPage()
215
+ )
216
+
217
+ // DefaultLayout.zen
218
+ fs.writeFileSync(
219
+ path.join(targetDir, baseDir, 'layouts', 'DefaultLayout.zen'),
220
+ generateDefaultLayout()
221
+ )
222
+
223
+ // global.css
224
+ fs.writeFileSync(
225
+ path.join(appDir, 'styles', 'global.css'),
226
+ generateGlobalCSS()
227
+ )
228
+
229
+ // .gitignore
230
+ fs.writeFileSync(
231
+ path.join(targetDir, '.gitignore'),
232
+ generateGitignore()
233
+ )
234
+ }
235
+
236
+ /**
237
+ * Generate configuration files based on options
238
+ */
239
+ async function generateConfigs(options: ProjectOptions): Promise<void> {
240
+ const targetDir = path.resolve(process.cwd(), options.name)
241
+
242
+ // tsconfig.json
243
+ const tsconfig: Record<string, unknown> = {
244
+ compilerOptions: {
245
+ target: 'ESNext',
246
+ module: 'ESNext',
247
+ moduleResolution: 'bundler',
248
+ strict: true,
249
+ esModuleInterop: true,
250
+ skipLibCheck: true,
251
+ forceConsistentCasingInFileNames: true,
252
+ resolveJsonModule: true,
253
+ declaration: true,
254
+ declarationMap: true,
255
+ noEmit: true
256
+ },
257
+ include: [options.directory + '/**/*', '*.ts'],
258
+ exclude: ['node_modules', 'dist']
259
+ }
260
+
261
+ if (options.pathAlias) {
262
+ (tsconfig.compilerOptions as Record<string, unknown>).baseUrl = '.'
263
+ ; (tsconfig.compilerOptions as Record<string, unknown>).paths = {
264
+ '@/*': [`./${options.directory}/*`]
265
+ }
266
+ }
267
+
268
+ fs.writeFileSync(
269
+ path.join(targetDir, 'tsconfig.json'),
270
+ JSON.stringify(tsconfig, null, 4)
271
+ )
272
+
273
+ // ESLint config
274
+ if (options.eslint) {
275
+ const eslintConfig = {
276
+ root: true,
277
+ parser: '@typescript-eslint/parser',
278
+ plugins: ['@typescript-eslint'],
279
+ extends: [
280
+ 'eslint:recommended',
281
+ 'plugin:@typescript-eslint/recommended'
282
+ ],
283
+ env: {
284
+ browser: true,
285
+ node: true,
286
+ es2022: true
287
+ },
288
+ rules: {
289
+ '@typescript-eslint/no-unused-vars': 'warn',
290
+ '@typescript-eslint/no-explicit-any': 'warn'
291
+ },
292
+ ignorePatterns: ['dist', 'node_modules']
293
+ }
294
+
295
+ fs.writeFileSync(
296
+ path.join(targetDir, '.eslintrc.json'),
297
+ JSON.stringify(eslintConfig, null, 4)
298
+ )
299
+ }
300
+
301
+ // Prettier config
302
+ if (options.prettier) {
303
+ const prettierConfig = {
304
+ semi: false,
305
+ singleQuote: true,
306
+ tabWidth: 4,
307
+ trailingComma: 'es5',
308
+ printWidth: 100
309
+ }
310
+
311
+ fs.writeFileSync(
312
+ path.join(targetDir, '.prettierrc'),
313
+ JSON.stringify(prettierConfig, null, 4)
314
+ )
315
+
316
+ fs.writeFileSync(
317
+ path.join(targetDir, '.prettierignore'),
318
+ 'dist\nnode_modules\nbun.lock\n'
319
+ )
320
+ }
321
+ }
322
+
323
+ // Template generators
324
+ function generateIndexPage(): string {
325
+ return `<script setup="ts">
326
+ state count = 0
327
+
328
+ function increment() {
329
+ count = count + 1
330
+ }
331
+
332
+ function decrement() {
333
+ count = count - 1
334
+ }
335
+
336
+ zenOnMount(() => {
337
+ console.log('🚀 Zenith app mounted!')
338
+ })
339
+ </script>
340
+
341
+ <DefaultLayout title="Zenith App">
342
+ <main>
343
+ <div class="hero">
344
+ <h1>Welcome to <span class="brand">Zenith</span></h1>
345
+ <p class="tagline">The Modern Reactive Web Framework</p>
346
+ </div>
347
+
348
+ <div class="counter-card">
349
+ <h2>Interactive Counter</h2>
350
+ <p class="count">{count}</p>
351
+ <div class="buttons">
352
+ <button onclick="decrement" class="btn-secondary">−</button>
353
+ <button onclick="increment" class="btn-primary">+</button>
354
+ </div>
355
+ </div>
356
+
357
+ <div class="features">
358
+ <div class="feature">
359
+ <span class="icon">⚡</span>
360
+ <h3>Reactive State</h3>
361
+ <p>Built-in state management with automatic DOM updates</p>
362
+ </div>
363
+ <div class="feature">
364
+ <span class="icon">🎯</span>
365
+ <h3>Zero Config</h3>
366
+ <p>Works immediately with no build step required</p>
367
+ </div>
368
+ <div class="feature">
369
+ <span class="icon">🔥</span>
370
+ <h3>Hot Reload</h3>
371
+ <p>Instant updates during development</p>
372
+ </div>
373
+ </div>
374
+ </div>
375
+ </DefaultLayout>
376
+
377
+ <style>
378
+ main {
379
+ max-width: 900px;
380
+ margin: 0 auto;
381
+ padding: 3rem 2rem;
382
+ font-family: system-ui, -apple-system, sans-serif;
383
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
384
+ color: #f1f5f9;
385
+ min-height: 100vh;
386
+ }
387
+
388
+ .hero {
389
+ text-align: center;
390
+ margin-bottom: 3rem;
391
+ }
392
+
393
+ h1 {
394
+ font-size: 3rem;
395
+ font-weight: 700;
396
+ margin-bottom: 0.5rem;
397
+ }
398
+
399
+ .brand {
400
+ background: linear-gradient(135deg, #3b82f6, #06b6d4);
401
+ -webkit-background-clip: text;
402
+ -webkit-text-fill-color: transparent;
403
+ background-clip: text;
404
+ }
405
+
406
+ .tagline {
407
+ color: #94a3b8;
408
+ font-size: 1.25rem;
409
+ }
410
+
411
+ .counter-card {
412
+ background: rgba(255, 255, 255, 0.05);
413
+ border: 1px solid rgba(255, 255, 255, 0.1);
414
+ border-radius: 16px;
415
+ padding: 2rem;
416
+ text-align: center;
417
+ margin-bottom: 3rem;
418
+ }
419
+
420
+ .counter-card h2 {
421
+ color: #e2e8f0;
422
+ margin-bottom: 1rem;
423
+ }
424
+
425
+ .count {
426
+ font-size: 4rem;
427
+ font-weight: 700;
428
+ color: #3b82f6;
429
+ margin: 1rem 0;
430
+ }
431
+
432
+ .buttons {
433
+ display: flex;
434
+ gap: 1rem;
435
+ justify-content: center;
436
+ }
437
+
438
+ button {
439
+ font-size: 1.5rem;
440
+ width: 60px;
441
+ height: 60px;
442
+ border: none;
443
+ border-radius: 12px;
444
+ cursor: pointer;
445
+ transition: all 0.2s ease;
446
+ font-weight: 600;
447
+ }
448
+
449
+ .btn-primary {
450
+ background: linear-gradient(135deg, #3b82f6, #2563eb);
451
+ color: white;
452
+ }
453
+
454
+ .btn-primary:hover {
455
+ transform: translateY(-2px);
456
+ box-shadow: 0 8px 20px rgba(59, 130, 246, 0.4);
457
+ }
458
+
459
+ .btn-secondary {
460
+ background: rgba(255, 255, 255, 0.1);
461
+ color: #e2e8f0;
462
+ }
463
+
464
+ .btn-secondary:hover {
465
+ background: rgba(255, 255, 255, 0.2);
466
+ }
467
+
468
+ .features {
469
+ display: grid;
470
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
471
+ gap: 1.5rem;
472
+ }
473
+
474
+ .feature {
475
+ background: rgba(255, 255, 255, 0.03);
476
+ border: 1px solid rgba(255, 255, 255, 0.06);
477
+ border-radius: 12px;
478
+ padding: 1.5rem;
479
+ text-align: center;
480
+ }
481
+
482
+ .icon {
483
+ font-size: 2rem;
484
+ display: block;
485
+ margin-bottom: 0.75rem;
486
+ }
487
+
488
+ .feature h3 {
489
+ color: #e2e8f0;
490
+ margin-bottom: 0.5rem;
491
+ font-size: 1.1rem;
492
+ }
493
+
494
+ .feature p {
495
+ color: #94a3b8;
496
+ font-size: 0.9rem;
497
+ line-height: 1.5;
498
+ }
499
+ </style>
500
+ `
501
+ }
502
+
503
+ function generateDefaultLayout(): string {
504
+ return `<script setup="ts">
505
+ // interface Props { title?: string; lang?: string }
506
+
507
+ zenEffect(() => {
508
+ document.title = title || 'Zenith App'
509
+ })
510
+
511
+ zenOnMount(() => {
512
+ console.log(\`[Layout] Mounted with title: \${title || 'Zenith App'}\`)
513
+ })
514
+ </script>
515
+
516
+ <html lang={lang || 'en'}>
517
+ <head>
518
+ <meta charset="UTF-8">
519
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
520
+ <link rel="stylesheet" href="/styles/global.css">
521
+ </head>
522
+ <body>
523
+ <div class="layout">
524
+ <header class="header">
525
+ <nav class="nav">
526
+ <a href="/" class="logo">⚡ Zenith</a>
527
+ <div class="nav-links">
528
+ <a href="/">Home</a>
529
+ <a href="/docs">Docs</a>
530
+ <a href="https://github.com/zenithbuild/zenith" target="_blank">GitHub</a>
531
+ </div>
532
+ </nav>
533
+ </header>
534
+
535
+ <main class="content">
536
+ <slot />
537
+ </main>
538
+
539
+ <footer class="footer">
540
+ <p>Built with ⚡ Zenith Framework</p>
541
+ </footer>
542
+ </div>
543
+ </body>
544
+ </html>
545
+
546
+ <style>
547
+ .layout {
548
+ min-height: 100vh;
549
+ display: flex;
550
+ flex-direction: column;
551
+ }
552
+
553
+ .header {
554
+ background: rgba(15, 23, 42, 0.95);
555
+ backdrop-filter: blur(10px);
556
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
557
+ position: sticky;
558
+ top: 0;
559
+ z-index: 100;
560
+ }
561
+
562
+ .nav {
563
+ max-width: 1200px;
564
+ margin: 0 auto;
565
+ padding: 1rem 2rem;
566
+ display: flex;
567
+ justify-content: space-between;
568
+ align-items: center;
569
+ }
570
+
571
+ .logo {
572
+ font-size: 1.25rem;
573
+ font-weight: 700;
574
+ color: #3b82f6;
575
+ text-decoration: none;
576
+ }
577
+
578
+ .nav-links {
579
+ display: flex;
580
+ gap: 2rem;
581
+ }
582
+
583
+ .nav-links a {
584
+ color: #94a3b8;
585
+ text-decoration: none;
586
+ transition: color 0.2s;
587
+ }
588
+
589
+ .nav-links a:hover {
590
+ color: #f1f5f9;
591
+ }
592
+
593
+ .content {
594
+ flex: 1;
595
+ }
596
+
597
+ .footer {
598
+ background: #0f172a;
599
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
600
+ padding: 2rem;
601
+ text-align: center;
602
+ color: #64748b;
603
+ }
604
+ </style>
605
+ `
606
+ }
607
+
608
+ function generateGlobalCSS(): string {
609
+ return `/* Zenith Global Styles */
610
+
611
+ /* CSS Reset */
612
+ *, *::before, *::after {
613
+ box-sizing: border-box;
614
+ margin: 0;
615
+ padding: 0;
616
+ }
617
+
618
+ html {
619
+ font-size: 16px;
620
+ -webkit-font-smoothing: antialiased;
621
+ -moz-osx-font-smoothing: grayscale;
622
+ }
623
+
624
+ body {
625
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
626
+ line-height: 1.6;
627
+ background: #0f172a;
628
+ color: #f1f5f9;
629
+ }
630
+
631
+ a {
632
+ color: #3b82f6;
633
+ text-decoration: none;
634
+ }
635
+
636
+ a:hover {
637
+ text-decoration: underline;
638
+ }
639
+
640
+ img, video {
641
+ max-width: 100%;
642
+ height: auto;
643
+ }
644
+
645
+ button, input, select, textarea {
646
+ font: inherit;
647
+ }
648
+
649
+ /* Utility Classes */
650
+ .container {
651
+ max-width: 1200px;
652
+ margin: 0 auto;
653
+ padding: 0 1rem;
654
+ }
655
+
656
+ .text-center { text-align: center; }
657
+ .text-muted { color: #94a3b8; }
658
+
659
+ /* Zenith Brand Colors */
660
+ :root {
661
+ --zen-primary: #3b82f6;
662
+ --zen-secondary: #06b6d4;
663
+ --zen-bg: #0f172a;
664
+ --zen-surface: #1e293b;
665
+ --zen-text: #f1f5f9;
666
+ --zen-muted: #94a3b8;
667
+ }
668
+ `
669
+ }
670
+
671
+ function generateGitignore(): string {
672
+ return `# Dependencies
673
+ node_modules/
674
+ bun.lock
675
+
676
+ # Build output
677
+ dist/
678
+ .cache/
679
+
680
+ # IDE
681
+ .idea/
682
+ .vscode/
683
+ *.swp
684
+ *.swo
685
+
686
+ # OS
687
+ .DS_Store
688
+ Thumbs.db
689
+
690
+ # Environment
691
+ .env
692
+ .env.local
693
+ .env.*.local
694
+
695
+ # Logs
696
+ *.log
697
+ npm-debug.log*
698
+
699
+ # Testing
700
+ coverage/
701
+ `
702
+ }