pulse-js-framework 1.7.11 → 1.7.13

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,1037 @@
1
+ /**
2
+ * Pulse CLI - Scaffold Command
3
+ * Generate components, pages, stores, and other project files
4
+ */
5
+
6
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
7
+ import { join, dirname, relative, basename } from 'path';
8
+ import { log } from './logger.js';
9
+ import { parseArgs } from './utils/file-utils.js';
10
+
11
+ /**
12
+ * Scaffold types and their templates
13
+ */
14
+ const SCAFFOLD_TYPES = {
15
+ component: {
16
+ description: 'Pulse component (.pulse file)',
17
+ defaultDir: 'src/components',
18
+ extension: '.pulse',
19
+ template: generateComponentTemplate
20
+ },
21
+ page: {
22
+ description: 'Page component with routing',
23
+ defaultDir: 'src/pages',
24
+ extension: '.pulse',
25
+ template: generatePageTemplate
26
+ },
27
+ store: {
28
+ description: 'State store module',
29
+ defaultDir: 'src/stores',
30
+ extension: '.js',
31
+ template: generateStoreTemplate
32
+ },
33
+ hook: {
34
+ description: 'Custom hook/composable',
35
+ defaultDir: 'src/hooks',
36
+ extension: '.js',
37
+ template: generateHookTemplate
38
+ },
39
+ service: {
40
+ description: 'Service/API module',
41
+ defaultDir: 'src/services',
42
+ extension: '.js',
43
+ template: generateServiceTemplate
44
+ },
45
+ test: {
46
+ description: 'Test file',
47
+ defaultDir: 'test',
48
+ extension: '.test.js',
49
+ template: generateTestTemplate
50
+ },
51
+ context: {
52
+ description: 'Context provider module',
53
+ defaultDir: 'src/contexts',
54
+ extension: '.js',
55
+ template: generateContextTemplate
56
+ },
57
+ layout: {
58
+ description: 'Layout component',
59
+ defaultDir: 'src/layouts',
60
+ extension: '.pulse',
61
+ template: generateLayoutTemplate
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Convert name to different cases
67
+ */
68
+ function toCase(name, type) {
69
+ // Remove extension if present
70
+ name = name.replace(/\.(pulse|js|ts)$/, '');
71
+
72
+ switch (type) {
73
+ case 'pascal':
74
+ return name
75
+ .replace(/[-_](.)/g, (_, c) => c.toUpperCase())
76
+ .replace(/^./, c => c.toUpperCase());
77
+ case 'camel':
78
+ return name
79
+ .replace(/[-_](.)/g, (_, c) => c.toUpperCase())
80
+ .replace(/^./, c => c.toLowerCase());
81
+ case 'kebab':
82
+ return name
83
+ .replace(/([A-Z])/g, '-$1')
84
+ .toLowerCase()
85
+ .replace(/^-/, '')
86
+ .replace(/[-_]+/g, '-');
87
+ case 'snake':
88
+ return name
89
+ .replace(/([A-Z])/g, '_$1')
90
+ .toLowerCase()
91
+ .replace(/^_/, '')
92
+ .replace(/[-_]+/g, '_');
93
+ default:
94
+ return name;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Generate component template
100
+ */
101
+ function generateComponentTemplate(name, options = {}) {
102
+ const pascalName = toCase(name, 'pascal');
103
+ const kebabName = toCase(name, 'kebab');
104
+ const { withState = true, withStyle = true, withProps = false } = options;
105
+
106
+ let template = `@page ${pascalName}\n`;
107
+
108
+ // Props section if requested
109
+ if (withProps) {
110
+ template += `
111
+ // Props (passed from parent)
112
+ // Usage: <${pascalName} title="Hello" count={5} />
113
+ props {
114
+ title: ""
115
+ count: 0
116
+ }
117
+ `;
118
+ }
119
+
120
+ // State section
121
+ if (withState) {
122
+ template += `
123
+ state {
124
+ // Add your state variables here
125
+ value: ""
126
+ }
127
+ `;
128
+ }
129
+
130
+ // View section
131
+ template += `
132
+ view {
133
+ .${kebabName} {
134
+ h2 "${pascalName}"
135
+ p "Your component content here"
136
+ ${withProps ? `
137
+ @if(title) {
138
+ p.title "{title}"
139
+ }` : ''}
140
+ }
141
+ }
142
+ `;
143
+
144
+ // Style section
145
+ if (withStyle) {
146
+ template += `
147
+ style {
148
+ .${kebabName} {
149
+ padding: 1rem
150
+
151
+ h2 {
152
+ margin-bottom: 0.5rem
153
+ }
154
+ }
155
+ }
156
+ `;
157
+ }
158
+
159
+ return template;
160
+ }
161
+
162
+ /**
163
+ * Generate page template
164
+ */
165
+ function generatePageTemplate(name, options = {}) {
166
+ const pascalName = toCase(name, 'pascal');
167
+ const kebabName = toCase(name, 'kebab');
168
+
169
+ return `@page ${pascalName}Page
170
+
171
+ // Route parameters are available via router.params
172
+ // import { router } from 'pulse-js-framework/runtime/router'
173
+
174
+ state {
175
+ loading: false
176
+ data: null
177
+ error: null
178
+ }
179
+
180
+ actions {
181
+ async loadData() {
182
+ loading = true
183
+ error = null
184
+ try {
185
+ // Load page data here
186
+ // const response = await fetch('/api/${kebabName}')
187
+ // data = await response.json()
188
+ } catch (e) {
189
+ error = e.message
190
+ } finally {
191
+ loading = false
192
+ }
193
+ }
194
+ }
195
+
196
+ view {
197
+ .${kebabName}-page {
198
+ header.page-header {
199
+ h1 "${pascalName}"
200
+ // Breadcrumb, actions, etc.
201
+ }
202
+
203
+ @if(loading) {
204
+ .loading-state {
205
+ p "Loading..."
206
+ }
207
+ }
208
+
209
+ @if(error) {
210
+ .error-state {
211
+ p.error "{error}"
212
+ button @click(loadData()) "Retry"
213
+ }
214
+ }
215
+
216
+ @if(!loading && !error) {
217
+ main.page-content {
218
+ // Page content here
219
+ p "Welcome to ${pascalName} page"
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ style {
226
+ .${kebabName}-page {
227
+ min-height: 100vh
228
+ padding: 2rem
229
+
230
+ .page-header {
231
+ margin-bottom: 2rem
232
+
233
+ h1 {
234
+ font-size: 2rem
235
+ margin: 0
236
+ }
237
+ }
238
+
239
+ .page-content {
240
+ max-width: 1200px
241
+ margin: 0 auto
242
+ }
243
+
244
+ .loading-state,
245
+ .error-state {
246
+ text-align: center
247
+ padding: 2rem
248
+ }
249
+
250
+ .error {
251
+ color: #dc3545
252
+ }
253
+ }
254
+ }
255
+ `;
256
+ }
257
+
258
+ /**
259
+ * Generate store template
260
+ */
261
+ function generateStoreTemplate(name, options = {}) {
262
+ const pascalName = toCase(name, 'pascal');
263
+ const camelName = toCase(name, 'camel');
264
+
265
+ return `/**
266
+ * ${pascalName} Store
267
+ * State management for ${name}
268
+ */
269
+
270
+ import { createStore, createActions } from 'pulse-js-framework/runtime/store';
271
+
272
+ /**
273
+ * Initial state
274
+ */
275
+ const initialState = {
276
+ items: [],
277
+ selectedId: null,
278
+ loading: false,
279
+ error: null
280
+ };
281
+
282
+ /**
283
+ * Create the store with persistence (optional)
284
+ */
285
+ export const ${camelName}Store = createStore(initialState, {
286
+ persist: false, // Set to true to persist to localStorage
287
+ name: '${kebabName}'
288
+ });
289
+
290
+ /**
291
+ * Store actions
292
+ */
293
+ export const ${camelName}Actions = createActions(${camelName}Store, {
294
+ /**
295
+ * Set items
296
+ * @param {Object} store - Store instance
297
+ * @param {Array} items - Items to set
298
+ */
299
+ setItems(store, items) {
300
+ store.items.set(items);
301
+ },
302
+
303
+ /**
304
+ * Add an item
305
+ * @param {Object} store - Store instance
306
+ * @param {Object} item - Item to add
307
+ */
308
+ addItem(store, item) {
309
+ store.items.update(items => [...items, item]);
310
+ },
311
+
312
+ /**
313
+ * Remove an item by ID
314
+ * @param {Object} store - Store instance
315
+ * @param {string|number} id - Item ID to remove
316
+ */
317
+ removeItem(store, id) {
318
+ store.items.update(items => items.filter(item => item.id !== id));
319
+ },
320
+
321
+ /**
322
+ * Update an item
323
+ * @param {Object} store - Store instance
324
+ * @param {string|number} id - Item ID
325
+ * @param {Object} updates - Fields to update
326
+ */
327
+ updateItem(store, id, updates) {
328
+ store.items.update(items =>
329
+ items.map(item => item.id === id ? { ...item, ...updates } : item)
330
+ );
331
+ },
332
+
333
+ /**
334
+ * Select an item
335
+ * @param {Object} store - Store instance
336
+ * @param {string|number|null} id - Item ID to select
337
+ */
338
+ select(store, id) {
339
+ store.selectedId.set(id);
340
+ },
341
+
342
+ /**
343
+ * Set loading state
344
+ * @param {Object} store - Store instance
345
+ * @param {boolean} loading - Loading state
346
+ */
347
+ setLoading(store, loading) {
348
+ store.loading.set(loading);
349
+ },
350
+
351
+ /**
352
+ * Set error
353
+ * @param {Object} store - Store instance
354
+ * @param {string|null} error - Error message
355
+ */
356
+ setError(store, error) {
357
+ store.error.set(error);
358
+ },
359
+
360
+ /**
361
+ * Clear all data
362
+ * @param {Object} store - Store instance
363
+ */
364
+ reset(store) {
365
+ store.items.set([]);
366
+ store.selectedId.set(null);
367
+ store.loading.set(false);
368
+ store.error.set(null);
369
+ }
370
+ });
371
+
372
+ /**
373
+ * Selectors (computed values)
374
+ */
375
+ export const ${camelName}Selectors = {
376
+ /**
377
+ * Get selected item
378
+ */
379
+ getSelected() {
380
+ const items = ${camelName}Store.items.get();
381
+ const selectedId = ${camelName}Store.selectedId.get();
382
+ return items.find(item => item.id === selectedId) || null;
383
+ },
384
+
385
+ /**
386
+ * Get item count
387
+ */
388
+ getCount() {
389
+ return ${camelName}Store.items.get().length;
390
+ },
391
+
392
+ /**
393
+ * Check if empty
394
+ */
395
+ isEmpty() {
396
+ return ${camelName}Store.items.get().length === 0;
397
+ }
398
+ };
399
+
400
+ // Export store parts for convenience
401
+ export const { items, selectedId, loading, error } = ${camelName}Store;
402
+ `;
403
+ }
404
+
405
+ /**
406
+ * Generate hook template
407
+ */
408
+ function generateHookTemplate(name, options = {}) {
409
+ const camelName = toCase(name, 'camel');
410
+ const hookName = camelName.startsWith('use') ? camelName : `use${toCase(name, 'pascal')}`;
411
+
412
+ return `/**
413
+ * ${hookName}
414
+ * Custom hook for ${name}
415
+ */
416
+
417
+ import { pulse, effect, computed } from 'pulse-js-framework/runtime';
418
+
419
+ /**
420
+ * ${hookName}
421
+ * @param {Object} options - Hook options
422
+ * @returns {Object} Hook return value
423
+ */
424
+ export function ${hookName}(options = {}) {
425
+ // State
426
+ const value = pulse(options.initialValue ?? null);
427
+ const loading = pulse(false);
428
+ const error = pulse(null);
429
+
430
+ // Computed values
431
+ const hasValue = computed(() => value.get() !== null);
432
+ const isReady = computed(() => !loading.get() && !error.get());
433
+
434
+ // Actions
435
+ function setValue(newValue) {
436
+ value.set(newValue);
437
+ }
438
+
439
+ function setLoading(isLoading) {
440
+ loading.set(isLoading);
441
+ }
442
+
443
+ function setError(err) {
444
+ error.set(err);
445
+ }
446
+
447
+ function reset() {
448
+ value.set(options.initialValue ?? null);
449
+ loading.set(false);
450
+ error.set(null);
451
+ }
452
+
453
+ // Optional: Setup effects
454
+ // effect(() => {
455
+ // const currentValue = value.get();
456
+ // // React to value changes
457
+ // });
458
+
459
+ // Return hook API
460
+ return {
461
+ // State (read-only pulses)
462
+ value,
463
+ loading,
464
+ error,
465
+
466
+ // Computed
467
+ hasValue,
468
+ isReady,
469
+
470
+ // Actions
471
+ setValue,
472
+ setLoading,
473
+ setError,
474
+ reset
475
+ };
476
+ }
477
+
478
+ export default ${hookName};
479
+ `;
480
+ }
481
+
482
+ /**
483
+ * Generate service template
484
+ */
485
+ function generateServiceTemplate(name, options = {}) {
486
+ const pascalName = toCase(name, 'pascal');
487
+ const camelName = toCase(name, 'camel');
488
+
489
+ return `/**
490
+ * ${pascalName} Service
491
+ * API and business logic for ${name}
492
+ */
493
+
494
+ import { createHttp } from 'pulse-js-framework/runtime/http';
495
+
496
+ // Create HTTP client for this service
497
+ const api = createHttp({
498
+ baseURL: '/api/${toCase(name, 'kebab')}',
499
+ timeout: 10000
500
+ });
501
+
502
+ /**
503
+ * ${pascalName} Service
504
+ */
505
+ export const ${camelName}Service = {
506
+ /**
507
+ * Get all items
508
+ * @param {Object} params - Query parameters
509
+ * @returns {Promise<Array>}
510
+ */
511
+ async getAll(params = {}) {
512
+ const response = await api.get('/', { params });
513
+ return response.data;
514
+ },
515
+
516
+ /**
517
+ * Get item by ID
518
+ * @param {string|number} id - Item ID
519
+ * @returns {Promise<Object>}
520
+ */
521
+ async getById(id) {
522
+ const response = await api.get(\`/\${id}\`);
523
+ return response.data;
524
+ },
525
+
526
+ /**
527
+ * Create new item
528
+ * @param {Object} data - Item data
529
+ * @returns {Promise<Object>}
530
+ */
531
+ async create(data) {
532
+ const response = await api.post('/', data);
533
+ return response.data;
534
+ },
535
+
536
+ /**
537
+ * Update item
538
+ * @param {string|number} id - Item ID
539
+ * @param {Object} data - Update data
540
+ * @returns {Promise<Object>}
541
+ */
542
+ async update(id, data) {
543
+ const response = await api.put(\`/\${id}\`, data);
544
+ return response.data;
545
+ },
546
+
547
+ /**
548
+ * Partial update item
549
+ * @param {string|number} id - Item ID
550
+ * @param {Object} data - Partial update data
551
+ * @returns {Promise<Object>}
552
+ */
553
+ async patch(id, data) {
554
+ const response = await api.patch(\`/\${id}\`, data);
555
+ return response.data;
556
+ },
557
+
558
+ /**
559
+ * Delete item
560
+ * @param {string|number} id - Item ID
561
+ * @returns {Promise<void>}
562
+ */
563
+ async delete(id) {
564
+ await api.delete(\`/\${id}\`);
565
+ },
566
+
567
+ /**
568
+ * Search items
569
+ * @param {string} query - Search query
570
+ * @param {Object} options - Search options
571
+ * @returns {Promise<Array>}
572
+ */
573
+ async search(query, options = {}) {
574
+ const response = await api.get('/search', {
575
+ params: { q: query, ...options }
576
+ });
577
+ return response.data;
578
+ }
579
+ };
580
+
581
+ export default ${camelName}Service;
582
+ `;
583
+ }
584
+
585
+ /**
586
+ * Generate test template
587
+ */
588
+ function generateTestTemplate(name, options = {}) {
589
+ const pascalName = toCase(name, 'pascal');
590
+ const camelName = toCase(name, 'camel');
591
+
592
+ return `/**
593
+ * ${pascalName} Tests
594
+ */
595
+
596
+ import { test, describe, beforeEach, afterEach } from 'node:test';
597
+ import assert from 'node:assert';
598
+
599
+ // Import the module to test
600
+ // import { ${camelName} } from '../src/${name}.js';
601
+
602
+ describe('${pascalName}', () => {
603
+ let instance;
604
+
605
+ beforeEach(() => {
606
+ // Setup before each test
607
+ instance = null;
608
+ });
609
+
610
+ afterEach(() => {
611
+ // Cleanup after each test
612
+ instance = null;
613
+ });
614
+
615
+ describe('initialization', () => {
616
+ test('should create instance with default options', () => {
617
+ // const result = ${camelName}();
618
+ // assert.ok(result);
619
+ assert.ok(true, 'placeholder');
620
+ });
621
+
622
+ test('should accept custom options', () => {
623
+ // const result = ${camelName}({ custom: true });
624
+ // assert.strictEqual(result.custom, true);
625
+ assert.ok(true, 'placeholder');
626
+ });
627
+ });
628
+
629
+ describe('core functionality', () => {
630
+ test('should perform main operation', () => {
631
+ // Add your test here
632
+ assert.ok(true, 'placeholder');
633
+ });
634
+
635
+ test('should handle edge cases', () => {
636
+ // Add your test here
637
+ assert.ok(true, 'placeholder');
638
+ });
639
+ });
640
+
641
+ describe('error handling', () => {
642
+ test('should throw on invalid input', () => {
643
+ // assert.throws(() => ${camelName}(null), { message: /invalid/i });
644
+ assert.ok(true, 'placeholder');
645
+ });
646
+ });
647
+ });
648
+ `;
649
+ }
650
+
651
+ /**
652
+ * Generate context template
653
+ */
654
+ function generateContextTemplate(name, options = {}) {
655
+ const pascalName = toCase(name, 'pascal');
656
+ const camelName = toCase(name, 'camel');
657
+
658
+ return `/**
659
+ * ${pascalName} Context
660
+ * Context provider for ${name}
661
+ */
662
+
663
+ import { createContext, useContext, Provider } from 'pulse-js-framework/runtime/context';
664
+ import { pulse, computed } from 'pulse-js-framework/runtime';
665
+
666
+ /**
667
+ * Default context value
668
+ */
669
+ const defaultValue = {
670
+ // Add default state here
671
+ };
672
+
673
+ /**
674
+ * Create the context
675
+ */
676
+ export const ${pascalName}Context = createContext(defaultValue, {
677
+ displayName: '${pascalName}Context'
678
+ });
679
+
680
+ /**
681
+ * ${pascalName} Provider Component
682
+ * @param {Object} props - Provider props
683
+ * @param {Function} props.children - Child render function
684
+ * @param {Object} props.value - Override context value
685
+ */
686
+ export function ${pascalName}Provider({ children, value = {} }) {
687
+ // Create reactive state
688
+ const state = pulse({
689
+ ...defaultValue,
690
+ ...value
691
+ });
692
+
693
+ // Create context value with state and actions
694
+ const contextValue = {
695
+ state,
696
+
697
+ // Actions
698
+ setValue: (newValue) => {
699
+ state.update(s => ({ ...s, ...newValue }));
700
+ },
701
+
702
+ reset: () => {
703
+ state.set({ ...defaultValue });
704
+ }
705
+ };
706
+
707
+ return Provider(${pascalName}Context, contextValue, children);
708
+ }
709
+
710
+ /**
711
+ * Hook to use ${pascalName} context
712
+ * @returns {Object} Context value
713
+ */
714
+ export function use${pascalName}() {
715
+ const context = useContext(${pascalName}Context);
716
+
717
+ if (!context || context === defaultValue) {
718
+ console.warn('use${pascalName} must be used within a ${pascalName}Provider');
719
+ }
720
+
721
+ return context;
722
+ }
723
+
724
+ /**
725
+ * Higher-order component to inject context
726
+ * @param {Function} Component - Component to wrap
727
+ * @returns {Function} Wrapped component
728
+ */
729
+ export function with${pascalName}(Component) {
730
+ return function Wrapped${pascalName}(props) {
731
+ const ${camelName} = use${pascalName}();
732
+ return Component({ ...props, ${camelName} });
733
+ };
734
+ }
735
+
736
+ export default ${pascalName}Context;
737
+ `;
738
+ }
739
+
740
+ /**
741
+ * Generate layout template
742
+ */
743
+ function generateLayoutTemplate(name, options = {}) {
744
+ const pascalName = toCase(name, 'pascal');
745
+ const kebabName = toCase(name, 'kebab');
746
+
747
+ return `@page ${pascalName}Layout
748
+
749
+ // Layout component with slots for header, main content, and footer
750
+
751
+ state {
752
+ sidebarOpen: false
753
+ }
754
+
755
+ actions {
756
+ toggleSidebar() {
757
+ sidebarOpen = !sidebarOpen
758
+ }
759
+ }
760
+
761
+ view {
762
+ .${kebabName}-layout {
763
+ // Header slot
764
+ header.layout-header {
765
+ slot "header" {
766
+ // Default header content
767
+ nav {
768
+ a[href="/"] "Home"
769
+ }
770
+ }
771
+
772
+ button.menu-toggle @click(toggleSidebar()) {
773
+ span.sr-only "Toggle menu"
774
+ span.icon "☰"
775
+ }
776
+ }
777
+
778
+ // Sidebar (optional)
779
+ aside.layout-sidebar[class.open=sidebarOpen] {
780
+ slot "sidebar" {
781
+ // Default sidebar content
782
+ nav {
783
+ ul {
784
+ li { a[href="/"] "Dashboard" }
785
+ li { a[href="/settings"] "Settings" }
786
+ }
787
+ }
788
+ }
789
+ }
790
+
791
+ // Main content slot
792
+ main.layout-main {
793
+ slot {
794
+ // Default main content
795
+ p "Content goes here"
796
+ }
797
+ }
798
+
799
+ // Footer slot
800
+ footer.layout-footer {
801
+ slot "footer" {
802
+ // Default footer content
803
+ p "© 2024"
804
+ }
805
+ }
806
+ }
807
+ }
808
+
809
+ style {
810
+ .${kebabName}-layout {
811
+ display: grid
812
+ grid-template-rows: auto 1fr auto
813
+ grid-template-columns: auto 1fr
814
+ min-height: 100vh
815
+
816
+ .layout-header {
817
+ grid-column: 1 / -1
818
+ display: flex
819
+ justify-content: space-between
820
+ align-items: center
821
+ padding: 1rem 2rem
822
+ background: #f5f5f5
823
+ border-bottom: 1px solid #ddd
824
+
825
+ nav a {
826
+ margin-right: 1rem
827
+ text-decoration: none
828
+ color: #333
829
+ }
830
+
831
+ .menu-toggle {
832
+ display: none
833
+ background: none
834
+ border: none
835
+ font-size: 1.5rem
836
+ cursor: pointer
837
+ }
838
+ }
839
+
840
+ .layout-sidebar {
841
+ width: 250px
842
+ padding: 1rem
843
+ background: #fafafa
844
+ border-right: 1px solid #eee
845
+
846
+ nav ul {
847
+ list-style: none
848
+ padding: 0
849
+ margin: 0
850
+ }
851
+
852
+ nav li {
853
+ margin-bottom: 0.5rem
854
+ }
855
+
856
+ nav a {
857
+ display: block
858
+ padding: 0.5rem
859
+ text-decoration: none
860
+ color: #333
861
+ border-radius: 4px
862
+ }
863
+
864
+ nav a:hover {
865
+ background: #eee
866
+ }
867
+ }
868
+
869
+ .layout-main {
870
+ padding: 2rem
871
+ overflow-y: auto
872
+ }
873
+
874
+ .layout-footer {
875
+ grid-column: 1 / -1
876
+ padding: 1rem 2rem
877
+ background: #f5f5f5
878
+ border-top: 1px solid #ddd
879
+ text-align: center
880
+ }
881
+
882
+ .sr-only {
883
+ position: absolute
884
+ width: 1px
885
+ height: 1px
886
+ padding: 0
887
+ margin: -1px
888
+ overflow: hidden
889
+ clip: rect(0, 0, 0, 0)
890
+ border: 0
891
+ }
892
+
893
+ @media (max-width: 768px) {
894
+ grid-template-columns: 1fr
895
+
896
+ .layout-header .menu-toggle {
897
+ display: block
898
+ }
899
+
900
+ .layout-sidebar {
901
+ position: fixed
902
+ left: -250px
903
+ top: 0
904
+ height: 100vh
905
+ z-index: 100
906
+ transition: left 0.3s ease
907
+ }
908
+
909
+ .layout-sidebar.open {
910
+ left: 0
911
+ }
912
+ }
913
+ }
914
+ }
915
+ `;
916
+ }
917
+
918
+ /**
919
+ * Main scaffold command handler
920
+ */
921
+ export async function runScaffold(args) {
922
+ const { options, patterns } = parseArgs(args);
923
+
924
+ // Parse arguments
925
+ const type = patterns[0];
926
+ const name = patterns[1];
927
+
928
+ // Show help if no type provided
929
+ if (!type) {
930
+ showScaffoldHelp();
931
+ return;
932
+ }
933
+
934
+ // Check if type is valid
935
+ if (!SCAFFOLD_TYPES[type]) {
936
+ log.error(`Unknown scaffold type: ${type}`);
937
+ log.info('\nAvailable types:');
938
+ for (const [key, info] of Object.entries(SCAFFOLD_TYPES)) {
939
+ log.info(` ${key.padEnd(12)} - ${info.description}`);
940
+ }
941
+ process.exit(1);
942
+ }
943
+
944
+ // Check if name is provided
945
+ if (!name) {
946
+ log.error(`Please provide a name for the ${type}.`);
947
+ log.info(`\nUsage: pulse scaffold ${type} <name>`);
948
+ process.exit(1);
949
+ }
950
+
951
+ const scaffoldType = SCAFFOLD_TYPES[type];
952
+
953
+ // Determine output directory
954
+ const dir = options.dir || options.d || scaffoldType.defaultDir;
955
+
956
+ // Determine file name
957
+ const fileName = toCase(name, type === 'component' || type === 'page' || type === 'layout' ? 'pascal' : 'camel');
958
+ const fullPath = join(process.cwd(), dir, fileName + scaffoldType.extension);
959
+
960
+ // Check if file already exists
961
+ if (existsSync(fullPath) && !options.force && !options.f) {
962
+ log.error(`File already exists: ${relative(process.cwd(), fullPath)}`);
963
+ log.info('Use --force to overwrite.');
964
+ process.exit(1);
965
+ }
966
+
967
+ // Generate content
968
+ const content = scaffoldType.template(name, {
969
+ withState: options.state !== false,
970
+ withStyle: options.style !== false,
971
+ withProps: options.props || false
972
+ });
973
+
974
+ // Create directory if needed
975
+ const outputDir = dirname(fullPath);
976
+ if (!existsSync(outputDir)) {
977
+ mkdirSync(outputDir, { recursive: true });
978
+ }
979
+
980
+ // Write file
981
+ writeFileSync(fullPath, content);
982
+
983
+ log.success(`Created ${type}: ${relative(process.cwd(), fullPath)}`);
984
+
985
+ // Show next steps
986
+ if (type === 'component' || type === 'page' || type === 'layout') {
987
+ log.info(`\nImport it in your app:`);
988
+ log.info(` import ${toCase(name, 'pascal')} from './${dir}/${fileName}${scaffoldType.extension}'`);
989
+ }
990
+ }
991
+
992
+ /**
993
+ * Show scaffold help
994
+ */
995
+ function showScaffoldHelp() {
996
+ log.info(`
997
+ Pulse Scaffold - Generate project files
998
+
999
+ Usage: pulse scaffold <type> <name> [options]
1000
+
1001
+ Types:
1002
+ `);
1003
+
1004
+ for (const [key, info] of Object.entries(SCAFFOLD_TYPES)) {
1005
+ log.info(` ${key.padEnd(12)} ${info.description}`);
1006
+ }
1007
+
1008
+ log.info(`
1009
+ Options:
1010
+ --dir, -d <path> Output directory (default: based on type)
1011
+ --force, -f Overwrite existing files
1012
+ --props Include props section (components)
1013
+ --no-state Skip state section
1014
+ --no-style Skip style section
1015
+
1016
+ Examples:
1017
+ pulse scaffold component Button
1018
+ pulse scaffold component UserCard --props
1019
+ pulse scaffold page Dashboard
1020
+ pulse scaffold store user
1021
+ pulse scaffold hook useAuth
1022
+ pulse scaffold service api
1023
+ pulse scaffold context Theme
1024
+ pulse scaffold layout Admin --dir src/layouts
1025
+ pulse scaffold test Button
1026
+
1027
+ Output Directories:
1028
+ component -> src/components/
1029
+ page -> src/pages/
1030
+ store -> src/stores/
1031
+ hook -> src/hooks/
1032
+ service -> src/services/
1033
+ context -> src/contexts/
1034
+ layout -> src/layouts/
1035
+ test -> test/
1036
+ `);
1037
+ }