@tachui/cli 0.7.0-alpha1

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,716 @@
1
+ /**
2
+ * Tacho CLI - Generate Command
3
+ *
4
+ * Code generation and scaffolding for TachUI components with Phase 6 patterns
5
+ */
6
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
8
+ import chalk from 'chalk';
9
+ import { Command } from 'commander';
10
+ import ora from 'ora';
11
+ import prompts from 'prompts';
12
+ const generators = {
13
+ component: {
14
+ name: 'Basic Component',
15
+ description: 'Generate a basic TachUI component with modifiers',
16
+ prompts: [
17
+ {
18
+ type: 'text',
19
+ name: 'description',
20
+ message: 'Component description:',
21
+ initial: 'A TachUI component',
22
+ },
23
+ {
24
+ type: 'confirm',
25
+ name: 'withState',
26
+ message: 'Include @State example?',
27
+ initial: true,
28
+ },
29
+ {
30
+ type: 'confirm',
31
+ name: 'withModifiers',
32
+ message: 'Include modifier examples?',
33
+ initial: true,
34
+ },
35
+ ],
36
+ generate: (answers, componentName) => ({
37
+ [`src/components/${componentName}.ts`]: `import { Layout, Text, Button } from '@tachui/core'${answers.withState
38
+ ? `
39
+ import { State } from '@tachui/core/state'`
40
+ : ''}
41
+
42
+ /**
43
+ * ${componentName}
44
+ *
45
+ * ${answers.description}
46
+ */
47
+ export function ${componentName}() {${answers.withState
48
+ ? `
49
+ const isActive = State(false)`
50
+ : ''}
51
+
52
+ return Layout.VStack({
53
+ children: [
54
+ Text('${componentName} Component')${answers.withModifiers
55
+ ? `
56
+ .modifier
57
+ .fontSize(20)
58
+ .fontWeight('bold')
59
+ .foregroundColor('#007AFF')
60
+ .build()`
61
+ : ''},${answers.withState
62
+ ? `
63
+
64
+ Text(() => \`Status: \${isActive.wrappedValue ? 'Active' : 'Inactive'}\`)${answers.withModifiers
65
+ ? `
66
+ .modifier
67
+ .fontSize(16)
68
+ .foregroundColor('#666')
69
+ .margin({ bottom: 16 })
70
+ .build()`
71
+ : ''},
72
+
73
+ Button({
74
+ title: 'Toggle State',
75
+ onTap: () => isActive.wrappedValue = !isActive.wrappedValue
76
+ })${answers.withModifiers
77
+ ? `
78
+ .modifier
79
+ .backgroundColor('#007AFF')
80
+ .foregroundColor('#ffffff')
81
+ .padding(12, 24)
82
+ .cornerRadius(8)
83
+ .build()`
84
+ : ''}`
85
+ : ''}
86
+ ],
87
+ spacing: 12,
88
+ alignment: 'center'
89
+ })${answers.withModifiers
90
+ ? `
91
+ .modifier
92
+ .padding(24)
93
+ .backgroundColor('#f8f9fa')
94
+ .cornerRadius(12)
95
+ .build()`
96
+ : ''}
97
+ }`,
98
+ }),
99
+ },
100
+ screen: {
101
+ name: 'Screen Component',
102
+ description: 'Generate a screen component with navigation support',
103
+ prompts: [
104
+ {
105
+ type: 'text',
106
+ name: 'description',
107
+ message: 'Screen description:',
108
+ initial: 'A TachUI screen',
109
+ },
110
+ {
111
+ type: 'confirm',
112
+ name: 'withNavigation',
113
+ message: 'Include navigation examples?',
114
+ initial: true,
115
+ },
116
+ {
117
+ type: 'confirm',
118
+ name: 'withLifecycle',
119
+ message: 'Include lifecycle modifiers?',
120
+ initial: true,
121
+ },
122
+ {
123
+ type: 'confirm',
124
+ name: 'withState',
125
+ message: 'Include state management?',
126
+ initial: true,
127
+ },
128
+ ],
129
+ generate: (answers, componentName) => ({
130
+ [`src/screens/${componentName}.ts`]: `import { Layout, Text, Button } from '@tachui/core'${answers.withState
131
+ ? `
132
+ import { State } from '@tachui/core/state'`
133
+ : ''}${answers.withNavigation
134
+ ? `
135
+ import { NavigationLink, useNavigation } from '@tachui/navigation'`
136
+ : ''}
137
+
138
+ /**
139
+ * ${componentName}
140
+ *
141
+ * ${answers.description}
142
+ */
143
+ export function ${componentName}() {${answers.withState
144
+ ? `
145
+ const isLoading = State(false)
146
+ const data = State<string>('')`
147
+ : ''}${answers.withNavigation
148
+ ? `
149
+ const navigation = useNavigation()`
150
+ : ''}
151
+
152
+ return Layout.VStack({
153
+ children: [
154
+ Text('${componentName}')
155
+ .modifier
156
+ .fontSize(28)
157
+ .fontWeight('bold')
158
+ .textAlign('center')
159
+ .margin({ bottom: 24 })
160
+ .build(),${answers.withState
161
+ ? `
162
+
163
+ Text(() => isLoading.wrappedValue ? 'Loading...' : 'Ready')
164
+ .modifier
165
+ .fontSize(18)
166
+ .foregroundColor('#666')
167
+ .margin({ bottom: 16 })
168
+ .build(),`
169
+ : ''}${answers.withNavigation
170
+ ? `
171
+
172
+ NavigationLink(
173
+ () => DetailScreen(),
174
+ Text('Go to Detail')
175
+ .modifier
176
+ .fontSize(16)
177
+ .foregroundColor('#007AFF')
178
+ .build()
179
+ ),
180
+
181
+ Button({
182
+ title: 'Go Back',
183
+ onTap: () => navigation.pop()
184
+ })
185
+ .modifier
186
+ .backgroundColor('#f0f0f0')
187
+ .foregroundColor('#333')
188
+ .padding(12, 24)
189
+ .cornerRadius(8)
190
+ .margin({ top: 16 })
191
+ .build()`
192
+ : ''}
193
+ ],
194
+ spacing: 0,
195
+ alignment: 'center'
196
+ })
197
+ .modifier
198
+ .padding(24)
199
+ .frame(undefined, '100vh')
200
+ .justifyContent('center')${answers.withLifecycle
201
+ ? `
202
+ .onAppear(() => {
203
+ console.log('${componentName} appeared')${answers.withState
204
+ ? `
205
+ isLoading.wrappedValue = true`
206
+ : ''}
207
+ })${answers.withState
208
+ ? `
209
+ .task(async () => {
210
+ // Simulate data loading
211
+ await new Promise(resolve => setTimeout(resolve, 1000))
212
+ data.wrappedValue = 'Loaded data for ${componentName}'
213
+ isLoading.wrappedValue = false
214
+ })`
215
+ : ''}`
216
+ : ''}
217
+ .build()
218
+ }`,
219
+ }),
220
+ },
221
+ store: {
222
+ name: 'Observable Store',
223
+ description: 'Generate a store class with @ObservedObject pattern',
224
+ prompts: [
225
+ {
226
+ type: 'text',
227
+ name: 'description',
228
+ message: 'Store description:',
229
+ initial: 'A data store',
230
+ },
231
+ {
232
+ type: 'text',
233
+ name: 'dataType',
234
+ message: 'Primary data type:',
235
+ initial: 'string',
236
+ },
237
+ {
238
+ type: 'confirm',
239
+ name: 'withCRUD',
240
+ message: 'Include CRUD operations?',
241
+ initial: true,
242
+ },
243
+ {
244
+ type: 'confirm',
245
+ name: 'withPersistence',
246
+ message: 'Include localStorage persistence?',
247
+ initial: false,
248
+ },
249
+ ],
250
+ generate: (answers, componentName) => {
251
+ const storeName = componentName;
252
+ // const itemName = componentName.replace(/Store$/, '')
253
+ return {
254
+ [`src/stores/${storeName}.ts`]: `import { ObservableObjectBase } from '@tachui/core/state'
255
+
256
+ /**
257
+ * ${storeName}
258
+ *
259
+ * ${answers.description}
260
+ */
261
+ export class ${storeName} extends ObservableObjectBase {
262
+ private _items: ${answers.dataType}[] = []${answers.withPersistence
263
+ ? `
264
+ private readonly STORAGE_KEY = '${storeName.toLowerCase()}_data'`
265
+ : ''}
266
+
267
+ constructor() {
268
+ super()${answers.withPersistence
269
+ ? `
270
+ this.loadFromStorage()`
271
+ : ''}
272
+ }
273
+
274
+ get items(): ${answers.dataType}[] {
275
+ return this._items
276
+ }
277
+
278
+ get count(): number {
279
+ return this._items.length
280
+ }${answers.withCRUD
281
+ ? `
282
+
283
+ add(item: ${answers.dataType}): void {
284
+ this._items.push(item)
285
+ this.notifyChange()${answers.withPersistence
286
+ ? `
287
+ this.saveToStorage()`
288
+ : ''}
289
+ }
290
+
291
+ remove(index: number): void {
292
+ if (index >= 0 && index < this._items.length) {
293
+ this._items.splice(index, 1)
294
+ this.notifyChange()${answers.withPersistence
295
+ ? `
296
+ this.saveToStorage()`
297
+ : ''}
298
+ }
299
+ }
300
+
301
+ update(index: number, item: ${answers.dataType}): void {
302
+ if (index >= 0 && index < this._items.length) {
303
+ this._items[index] = item
304
+ this.notifyChange()${answers.withPersistence
305
+ ? `
306
+ this.saveToStorage()`
307
+ : ''}
308
+ }
309
+ }
310
+
311
+ clear(): void {
312
+ this._items = []
313
+ this.notifyChange()${answers.withPersistence
314
+ ? `
315
+ this.saveToStorage()`
316
+ : ''}
317
+ }`
318
+ : ''}${answers.withPersistence
319
+ ? `
320
+
321
+ private saveToStorage(): void {
322
+ try {
323
+ localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this._items))
324
+ } catch (error) {
325
+ console.warn('Failed to save to localStorage:', error)
326
+ }
327
+ }
328
+
329
+ private loadFromStorage(): void {
330
+ try {
331
+ const stored = localStorage.getItem(this.STORAGE_KEY)
332
+ if (stored) {
333
+ this._items = JSON.parse(stored)
334
+ }
335
+ } catch (error) {
336
+ console.warn('Failed to load from localStorage:', error)
337
+ this._items = []
338
+ }
339
+ }`
340
+ : ''}
341
+ }
342
+
343
+ // Example usage:
344
+ // const store = new ${storeName}()
345
+ // const observedStore = ObservedObject(store)
346
+ //
347
+ // In component:
348
+ // observedStore.wrappedValue.add("new item")
349
+ `,
350
+ };
351
+ },
352
+ },
353
+ form: {
354
+ name: 'Form Component',
355
+ description: 'Generate a form component with validation',
356
+ prompts: [
357
+ {
358
+ type: 'text',
359
+ name: 'description',
360
+ message: 'Form description:',
361
+ initial: 'A form component',
362
+ },
363
+ {
364
+ type: 'confirm',
365
+ name: 'withValidation',
366
+ message: 'Include form validation?',
367
+ initial: true,
368
+ },
369
+ {
370
+ type: 'confirm',
371
+ name: 'withSubmission',
372
+ message: 'Include form submission handler?',
373
+ initial: true,
374
+ },
375
+ ],
376
+ generate: (answers, componentName) => ({
377
+ [`src/components/${componentName}.ts`]: `import { Layout, Text, Button } from '@tachui/core'
378
+ import { TextField } from '@tachui/forms'
379
+ import { State } from '@tachui/core/state'
380
+
381
+ /**
382
+ * ${componentName}
383
+ *
384
+ * ${answers.description}
385
+ */
386
+ export function ${componentName}() {
387
+ const formData = State({
388
+ name: '',
389
+ email: '',
390
+ message: ''
391
+ })
392
+
393
+ const isSubmitting = State(false)${answers.withValidation
394
+ ? `
395
+ const errors = State<Record<string, string>>({})`
396
+ : ''}${answers.withValidation
397
+ ? `
398
+
399
+ const validateForm = () => {
400
+ const newErrors: Record<string, string> = {}
401
+
402
+ if (!formData.wrappedValue.name.trim()) {
403
+ newErrors.name = 'Name is required'
404
+ }
405
+
406
+ if (!formData.wrappedValue.email.trim()) {
407
+ newErrors.email = 'Email is required'
408
+ } else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(formData.wrappedValue.email)) {
409
+ newErrors.email = 'Invalid email format'
410
+ }
411
+
412
+ if (!formData.wrappedValue.message.trim()) {
413
+ newErrors.message = 'Message is required'
414
+ }
415
+
416
+ errors.wrappedValue = newErrors
417
+ return Object.keys(newErrors).length === 0
418
+ }`
419
+ : ''}${answers.withSubmission
420
+ ? `
421
+
422
+ const handleSubmit = async () => {${answers.withValidation
423
+ ? `
424
+ if (!validateForm()) {
425
+ return
426
+ }`
427
+ : ''}
428
+
429
+ isSubmitting.wrappedValue = true
430
+
431
+ try {
432
+ // Simulate form submission
433
+ await new Promise(resolve => setTimeout(resolve, 2000))
434
+
435
+ console.log('Form submitted:', formData.wrappedValue)
436
+
437
+ // Reset form
438
+ formData.wrappedValue = {
439
+ name: '',
440
+ email: '',
441
+ message: ''
442
+ }${answers.withValidation
443
+ ? `
444
+
445
+ errors.wrappedValue = {}`
446
+ : ''}
447
+
448
+ } catch (error) {
449
+ console.error('Submission failed:', error)
450
+ } finally {
451
+ isSubmitting.wrappedValue = false
452
+ }
453
+ }`
454
+ : ''}
455
+
456
+ return Layout.VStack({
457
+ children: [
458
+ Text('${componentName}')
459
+ .modifier
460
+ .fontSize(24)
461
+ .fontWeight('bold')
462
+ .margin({ bottom: 24 })
463
+ .build(),
464
+
465
+ // Name field
466
+ Layout.VStack({
467
+ children: [
468
+ Text('Name')
469
+ .modifier
470
+ .fontSize(16)
471
+ .fontWeight('medium')
472
+ .margin({ bottom: 8 })
473
+ .build(),
474
+
475
+ TextField({
476
+ placeholder: 'Enter your name',
477
+ text: formData.projectedValue.map(
478
+ (data) => data.name,
479
+ (newName, data) => ({ ...data, name: newName })
480
+ )
481
+ })
482
+ .modifier
483
+ .padding(12)
484
+ .border('1px solid #ddd')
485
+ .cornerRadius(8)
486
+ .build(),${answers.withValidation
487
+ ? `
488
+
489
+ ...(errors.wrappedValue.name ? [
490
+ Text(errors.wrappedValue.name)
491
+ .modifier
492
+ .fontSize(14)
493
+ .foregroundColor('#ef4444')
494
+ .margin({ top: 4 })
495
+ .build()
496
+ ] : [])`
497
+ : ''}
498
+ ],
499
+ spacing: 0,
500
+ alignment: 'leading'
501
+ }),
502
+
503
+ // Email field
504
+ Layout.VStack({
505
+ children: [
506
+ Text('Email')
507
+ .modifier
508
+ .fontSize(16)
509
+ .fontWeight('medium')
510
+ .margin({ bottom: 8 })
511
+ .build(),
512
+
513
+ TextField({
514
+ placeholder: 'Enter your email',
515
+ text: formData.projectedValue.map(
516
+ (data) => data.email,
517
+ (newEmail, data) => ({ ...data, email: newEmail })
518
+ )
519
+ })
520
+ .modifier
521
+ .padding(12)
522
+ .border('1px solid #ddd')
523
+ .cornerRadius(8)
524
+ .build(),${answers.withValidation
525
+ ? `
526
+
527
+ ...(errors.wrappedValue.email ? [
528
+ Text(errors.wrappedValue.email)
529
+ .modifier
530
+ .fontSize(14)
531
+ .foregroundColor('#ef4444')
532
+ .margin({ top: 4 })
533
+ .build()
534
+ ] : [])`
535
+ : ''}
536
+ ],
537
+ spacing: 0,
538
+ alignment: 'leading'
539
+ }),
540
+
541
+ // Message field
542
+ Layout.VStack({
543
+ children: [
544
+ Text('Message')
545
+ .modifier
546
+ .fontSize(16)
547
+ .fontWeight('medium')
548
+ .margin({ bottom: 8 })
549
+ .build(),
550
+
551
+ TextField({
552
+ placeholder: 'Enter your message',
553
+ text: formData.projectedValue.map(
554
+ (data) => data.message,
555
+ (newMessage, data) => ({ ...data, message: newMessage })
556
+ )
557
+ })
558
+ .modifier
559
+ .padding(12)
560
+ .border('1px solid #ddd')
561
+ .cornerRadius(8)
562
+ .minHeight(100)
563
+ .build(),${answers.withValidation
564
+ ? `
565
+
566
+ ...(errors.wrappedValue.message ? [
567
+ Text(errors.wrappedValue.message)
568
+ .modifier
569
+ .fontSize(14)
570
+ .foregroundColor('#ef4444')
571
+ .margin({ top: 4 })
572
+ .build()
573
+ ] : [])`
574
+ : ''}
575
+ ],
576
+ spacing: 0,
577
+ alignment: 'leading'
578
+ }),
579
+
580
+ // Submit button
581
+ Button({
582
+ title: isSubmitting.wrappedValue ? 'Submitting...' : 'Submit',
583
+ onTap: ${answers.withSubmission ? 'handleSubmit' : '() => console.log("Form submitted:", formData.wrappedValue)'},
584
+ disabled: isSubmitting.wrappedValue
585
+ })
586
+ .modifier
587
+ .backgroundColor(isSubmitting.wrappedValue ? '#ccc' : '#007AFF')
588
+ .foregroundColor('#ffffff')
589
+ .padding(16, 32)
590
+ .cornerRadius(8)
591
+ .margin({ top: 24 })
592
+ .build()
593
+ ],
594
+ spacing: 16,
595
+ alignment: 'stretch'
596
+ })
597
+ .modifier
598
+ .padding(24)
599
+ .maxWidth(500)
600
+ .build()
601
+ }`,
602
+ }),
603
+ },
604
+ };
605
+ export const generateCommand = new Command('generate')
606
+ .description('Generate TachUI components and code')
607
+ .alias('g')
608
+ .argument('[type]', 'Generator type (component, screen, store, form)')
609
+ .argument('[name]', 'Component name')
610
+ .option('-y, --yes', 'Skip prompts and use defaults')
611
+ .option('-d, --dir <directory>', 'Output directory')
612
+ .action(async (type, name, options) => {
613
+ try {
614
+ let selectedType = type;
615
+ let componentName = name;
616
+ // Show available generators if no type specified
617
+ if (!selectedType) {
618
+ const response = await prompts({
619
+ type: 'select',
620
+ name: 'type',
621
+ message: 'What would you like to generate?',
622
+ choices: Object.entries(generators).map(([key, generator]) => ({
623
+ title: generator.name,
624
+ description: generator.description,
625
+ value: key,
626
+ })),
627
+ });
628
+ if (!response.type) {
629
+ console.log(chalk.yellow('Operation cancelled'));
630
+ return;
631
+ }
632
+ selectedType = response.type;
633
+ }
634
+ const generator = generators[selectedType];
635
+ if (!generator) {
636
+ console.error(chalk.red(`Generator "${selectedType}" not found`));
637
+ console.log(chalk.yellow('Available generators:'), Object.keys(generators).join(', '));
638
+ return;
639
+ }
640
+ // Get component name if not provided
641
+ if (!componentName) {
642
+ const response = await prompts({
643
+ type: 'text',
644
+ name: 'name',
645
+ message: `${generator.name} name:`,
646
+ validate: (value) => {
647
+ if (!value.trim())
648
+ return 'Name is required';
649
+ if (!/^[A-Z][a-zA-Z0-9]*$/.test(value)) {
650
+ return 'Name must start with capital letter and contain only letters/numbers';
651
+ }
652
+ return true;
653
+ },
654
+ });
655
+ if (!response.name) {
656
+ console.log(chalk.yellow('Operation cancelled'));
657
+ return;
658
+ }
659
+ componentName = response.name;
660
+ }
661
+ // Validate component name
662
+ if (!componentName || !/^[A-Z][a-zA-Z0-9]*$/.test(componentName)) {
663
+ console.error(chalk.red('Component name must start with capital letter and contain only letters/numbers'));
664
+ return;
665
+ }
666
+ // Get generator-specific configuration
667
+ let answers = {};
668
+ if (!options?.yes && generator.prompts.length > 0) {
669
+ answers = await prompts(generator.prompts);
670
+ }
671
+ const spinner = ora(`Generating ${generator.name}...`).start();
672
+ // Generate files
673
+ const files = generator.generate(answers, componentName);
674
+ const baseDir = options?.dir || process.cwd();
675
+ for (const [filePath, content] of Object.entries(files)) {
676
+ const fullPath = resolve(baseDir, filePath);
677
+ const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
678
+ // Create directory if it doesn't exist
679
+ if (!existsSync(dir)) {
680
+ mkdirSync(dir, { recursive: true });
681
+ }
682
+ // Check if file already exists
683
+ if (existsSync(fullPath)) {
684
+ const overwrite = await prompts({
685
+ type: 'confirm',
686
+ name: 'overwrite',
687
+ message: `File ${filePath} already exists. Overwrite?`,
688
+ initial: false,
689
+ });
690
+ if (!overwrite.overwrite) {
691
+ continue;
692
+ }
693
+ }
694
+ writeFileSync(fullPath, content);
695
+ }
696
+ spinner.succeed(`${generator.name} generated successfully!`);
697
+ // Show created files
698
+ console.log(`\n${chalk.green('✅ Files created:')}`);
699
+ Object.keys(files).forEach((filePath) => {
700
+ console.log(chalk.gray(` ${filePath}`));
701
+ });
702
+ // Show usage instructions
703
+ console.log(`\n${chalk.yellow('💡 Usage:')}`);
704
+ const importPath = Object.keys(files)[0]
705
+ .replace(/^src\//, './')
706
+ .replace(/\.ts$/, '');
707
+ console.log(chalk.gray(` import { ${componentName} } from '${importPath}'`));
708
+ console.log(chalk.gray(` // Use ${componentName}() in your app`));
709
+ console.log(`\n${chalk.green('Happy coding with TachUI! 🚀')}`);
710
+ }
711
+ catch (error) {
712
+ console.error(chalk.red('Error generating code:'), error.message);
713
+ process.exit(1);
714
+ }
715
+ });
716
+ //# sourceMappingURL=generate.js.map