@tachui/cli
Version:
Tacho CLI - Comprehensive developer tooling for tachUI
734 lines (667 loc) • 21 kB
JavaScript
/**
* Tacho CLI - Init Command
*
* Initialize new TachUI projects with smart templates and Phase 6 features
*/
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import chalk from 'chalk';
import { Command } from 'commander';
import ora from 'ora';
import prompts from 'prompts';
const templates = {
basic: {
name: 'Basic TachUI App',
description: 'Simple TachUI application with core components',
features: ['Text & Button components', 'Layout system', 'Modifiers'],
files: {
'package.json': JSON.stringify({
name: '{projectName}',
version: '1.0.0',
description: 'TachUI application',
type: 'module',
scripts: {
dev: 'vite',
build: 'vite build',
preview: 'vite preview',
typecheck: 'tsc --noEmit',
},
dependencies: {
'@tachui/core': '^0.1.0',
'@tachui/forms': '^0.1.0',
},
devDependencies: {
vite: '^5.0.0',
typescript: '^5.0.0',
'@types/node': '^20.0.0',
},
}, null, 2),
'vite.config.ts': `import { defineConfig } from 'vite'
export default defineConfig({
server: {
port: 3000,
open: true
},
build: {
target: 'es2020'
}
})`,
'tsconfig.json': JSON.stringify({
compilerOptions: {
target: 'ES2020',
module: 'ESNext',
moduleResolution: 'bundler',
allowSyntheticDefaultImports: true,
esModuleInterop: true,
jsx: 'preserve',
declaration: true,
strict: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
},
include: ['src/**/*'],
exclude: ['node_modules', 'dist'],
}, null, 2),
'src/main.ts': `import { mount } from '@tachui/core'
import { App } from './App'
// Mount the app
mount('#app', App())`,
'src/App.ts': `import { Layout, Text, Button } from '@tachui/core'
export function App() {
return Layout.VStack({
children: [
Text('Welcome to TachUI!')
.modifier
.fontSize(32)
.fontWeight('bold')
.foregroundColor('#007AFF')
.margin({ bottom: 16 })
.build(),
Text('SwiftUI-inspired web development')
.modifier
.fontSize(18)
.foregroundColor('#666')
.margin({ bottom: 24 })
.build(),
Button({
title: 'Get Started',
onTap: () => console.log('Hello TachUI!')
})
.modifier
.backgroundColor('#007AFF')
.foregroundColor('#ffffff')
.padding(16, 32)
.cornerRadius(8)
.build()
],
spacing: 0,
alignment: 'center'
})
.modifier
.frame(undefined, '100vh')
.justifyContent('center')
.alignItems('center')
.backgroundColor('#f5f5f7')
.build()
}`,
'index.html': `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{projectName}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>`,
'README.md': `# {projectName}
A TachUI application built with SwiftUI-inspired components and reactive architecture.
## Getting Started
\`\`\`bash
npm install
npm run dev
\`\`\`
## Available Scripts
- \`npm run dev\` - Start development server
- \`npm run build\` - Build for production
- \`npm run preview\` - Preview production build
- \`npm run typecheck\` - Type check without building
## Learn More
- [TachUI Documentation](https://github.com/whoughton/TachUI)
- [TachUI Examples](https://github.com/whoughton/TachUI/tree/main/examples)
`,
},
},
phase6: {
name: 'Phase 6 Features App',
description: 'Complete app with state management, lifecycle, and navigation',
features: [
'@State & @ObservedObject',
'Lifecycle modifiers',
'NavigationView & TabView',
'Real-world patterns',
],
files: {
'package.json': JSON.stringify({
name: '{projectName}',
version: '1.0.0',
description: 'TachUI Phase 6 application with advanced features',
type: 'module',
scripts: {
dev: 'vite',
build: 'vite build',
preview: 'vite preview',
typecheck: 'tsc --noEmit',
},
dependencies: {
'@tachui/core': '^0.1.0',
'@tachui/forms': '^0.1.0',
},
devDependencies: {
vite: '^5.0.0',
typescript: '^5.0.0',
'@types/node': '^20.0.0',
},
}, null, 2),
'vite.config.ts': `import { defineConfig } from 'vite'
export default defineConfig({
server: {
port: 3000,
open: true
},
build: {
target: 'es2020'
}
})`,
'tsconfig.json': JSON.stringify({
compilerOptions: {
target: 'ES2020',
module: 'ESNext',
moduleResolution: 'bundler',
allowSyntheticDefaultImports: true,
esModuleInterop: true,
jsx: 'preserve',
declaration: true,
strict: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
},
include: ['src/**/*'],
exclude: ['node_modules', 'dist'],
}, null, 2),
'src/main.ts': `import { mount } from '@tachui/core'
import { App } from './App'
// Mount the app
mount('#app', App())`,
'src/App.ts': `import { TabView, createTabItem } from '@tachui/core/navigation'
import { State } from '@tachui/core/state'
import { HomeScreen } from './screens/HomeScreen'
import { TodoScreen } from './screens/TodoScreen'
import { SettingsScreen } from './screens/SettingsScreen'
export function App() {
const selectedTab = State('home')
const tabs = [
createTabItem(
'home',
'Home',
HomeScreen(),
{ icon: '🏠' }
),
createTabItem(
'todos',
'Todos',
TodoScreen(),
{ icon: '📝' }
),
createTabItem(
'settings',
'Settings',
SettingsScreen(),
{ icon: '⚙️' }
)
]
return TabView(tabs, {
selection: selectedTab.projectedValue,
tabPlacement: 'bottom',
accentColor: '#007AFF'
})
}`,
'src/screens/HomeScreen.ts': `import { Layout, Text, Button } from '@tachui/core'
import { State } from '@tachui/core/state'
export function HomeScreen() {
const welcomeMessage = State('Welcome to TachUI Phase 6!')
const clickCount = State(0)
return Layout.VStack({
children: [
Text(() => welcomeMessage.wrappedValue)
.modifier
.fontSize(28)
.fontWeight('bold')
.foregroundColor('#007AFF')
.textAlign('center')
.margin({ bottom: 24 })
.build(),
Text('This app demonstrates all Phase 6 features:')
.modifier
.fontSize(18)
.foregroundColor('#333')
.margin({ bottom: 16 })
.build(),
Layout.VStack({
children: [
Text('✅ @State reactive property wrapper'),
Text('✅ Lifecycle modifiers (onAppear, task)'),
Text('✅ TabView navigation system'),
Text('✅ Real-world component patterns')
].map(text =>
text.modifier
.fontSize(16)
.foregroundColor('#666')
.padding({ vertical: 4 })
.build()
),
spacing: 4,
alignment: 'leading'
}),
Button({
title: \`Clicked \${() => clickCount.wrappedValue} times\`,
onTap: () => clickCount.wrappedValue++
})
.modifier
.backgroundColor('#007AFF')
.foregroundColor('#ffffff')
.padding(16, 24)
.cornerRadius(8)
.margin({ top: 32 })
.build()
],
spacing: 0,
alignment: 'center'
})
.modifier
.padding(24)
.onAppear(() => {
console.log('Home screen appeared!')
})
.task(async () => {
// Simulate loading welcome message
await new Promise(resolve => setTimeout(resolve, 1000))
welcomeMessage.wrappedValue = 'Welcome to TachUI Phase 6! 🚀'
})
.build()
}`,
'src/screens/TodoScreen.ts': `import { Layout, Text, Button } from '@tachui/core'
import { TextField } from '@tachui/forms'
import { State, ObservableObjectBase, ObservedObject } from '@tachui/core/state'
class TodoItem extends ObservableObjectBase {
constructor(
public id: string,
private _text: string,
private _completed: boolean = false
) {
super()
}
get text() { return this._text }
set text(value: string) {
this._text = value
this.notifyChange()
}
get completed() { return this._completed }
set completed(value: boolean) {
this._completed = value
this.notifyChange()
}
toggle() {
this.completed = !this.completed
}
}
class TodoStore extends ObservableObjectBase {
private _items: TodoItem[] = []
get items() { return this._items }
addItem(text: string) {
const item = new TodoItem(Date.now().toString(), text)
this._items.push(item)
this.notifyChange()
}
removeItem(id: string) {
this._items = this._items.filter(item => item.id !== id)
this.notifyChange()
}
get completedCount() {
return this._items.filter(item => item.completed).length
}
}
export function TodoScreen() {
const todoStore = ObservedObject(new TodoStore())
const newTodoText = State('')
const addTodo = () => {
if (newTodoText.wrappedValue.trim()) {
todoStore.wrappedValue.addItem(newTodoText.wrappedValue)
newTodoText.wrappedValue = ''
}
}
return Layout.VStack({
children: [
Text('My Todos')
.modifier
.fontSize(28)
.fontWeight('bold')
.margin({ bottom: 16 })
.build(),
Text(() => \`\${todoStore.wrappedValue.completedCount} of \${todoStore.wrappedValue.items.length} completed\`)
.modifier
.fontSize(16)
.foregroundColor('#666')
.margin({ bottom: 20 })
.build(),
Layout.HStack({
children: [
TextField({
placeholder: 'Enter new todo',
text: newTodoText.projectedValue
})
.modifier
.flexGrow(1)
.build(),
Button({
title: 'Add',
onTap: addTodo
})
.modifier
.backgroundColor('#007AFF')
.foregroundColor('#ffffff')
.padding(8, 16)
.cornerRadius(6)
.build()
],
spacing: 12
}),
Layout.VStack({
children: todoStore.wrappedValue.items.map(item => {
const observedItem = ObservedObject(item)
return Layout.HStack({
children: [
Button({
title: observedItem.wrappedValue.completed ? '✅' : '⬜',
onTap: () => observedItem.wrappedValue.toggle()
})
.modifier
.backgroundColor('transparent')
.border(0)
.padding(0)
.build(),
Text(() => observedItem.wrappedValue.text)
.modifier
.fontSize(16)
.foregroundColor(observedItem.wrappedValue.completed ? '#999' : '#333')
.textDecoration(observedItem.wrappedValue.completed ? 'line-through' : 'none')
.flexGrow(1)
.build(),
Button({
title: '🗑️',
onTap: () => todoStore.wrappedValue.removeItem(observedItem.wrappedValue.id)
})
.modifier
.backgroundColor('transparent')
.border(0)
.padding(0)
.build()
],
spacing: 12,
alignment: 'center'
})
.modifier
.backgroundColor('#f8f9fa')
.padding(12)
.cornerRadius(8)
.margin({ bottom: 8 })
.build()
}),
spacing: 0
})
.modifier
.margin({ top: 20 })
.build()
],
spacing: 0
})
.modifier
.padding(24)
.build()
}`,
'src/screens/SettingsScreen.ts': `import { Layout, Text } from '@tachui/core'
import { State } from '@tachui/core/state'
export function SettingsScreen() {
const version = State('1.0.0')
return Layout.VStack({
children: [
Text('Settings')
.modifier
.fontSize(28)
.fontWeight('bold')
.margin({ bottom: 32 })
.build(),
Layout.VStack({
children: [
Text('App Information')
.modifier
.fontSize(20)
.fontWeight('semibold')
.margin({ bottom: 16 })
.build(),
Layout.HStack({
children: [
Text('Version:')
.modifier
.fontSize(16)
.foregroundColor('#666')
.build(),
Text(() => version.wrappedValue)
.modifier
.fontSize(16)
.fontWeight('medium')
.build()
],
spacing: 8,
alignment: 'center'
}),
Layout.HStack({
children: [
Text('Framework:')
.modifier
.fontSize(16)
.foregroundColor('#666')
.build(),
Text('TachUI Phase 6')
.modifier
.fontSize(16)
.fontWeight('medium')
.build()
],
spacing: 8,
alignment: 'center'
}),
Text('Built with SwiftUI-inspired components and reactive state management')
.modifier
.fontSize(14)
.foregroundColor('#999')
.textAlign('center')
.margin({ top: 24 })
.build()
],
spacing: 12,
alignment: 'leading'
})
],
spacing: 0
})
.modifier
.padding(24)
.build()
}`,
'index.html': `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{projectName}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f7;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>`,
'README.md': `# {projectName}
A complete TachUI application showcasing Phase 6 features:
- **@State**: Reactive local state management
- **@ObservedObject**: External object observation
- **Lifecycle Modifiers**: onAppear, task, refreshable
- **Navigation**: TabView with multiple screens
- **Real-world Patterns**: Todo app with persistent state
## Getting Started
\`\`\`bash
npm install
npm run dev
\`\`\`
## Features Demonstrated
### State Management
- Local reactive state with \`@State\`
- Observable objects with \`@ObservedObject\`
- Property wrapper patterns from SwiftUI
### Lifecycle Management
- \`onAppear\` for component initialization
- \`task\` for async operations with automatic cancellation
- Component lifecycle integration
### Navigation System
- \`TabView\` for tab-based navigation
- Multiple screens with state preservation
- SwiftUI-style navigation patterns
### Real-world Patterns
- Todo application with CRUD operations
- Observable data models
- Reactive UI updates
- Component composition
## Available Scripts
- \`npm run dev\` - Start development server
- \`npm run build\` - Build for production
- \`npm run preview\` - Preview production build
- \`npm run typecheck\` - Type check without building
## Learn More
- [TachUI Documentation](https://github.com/whoughton/TachUI)
- [Phase 6 Features Guide](https://github.com/whoughton/TachUI/blob/main/docs/api/phase-6-features.md)
`,
},
},
};
export const initCommand = new Command('init')
.description('Initialize a new TachUI project')
.argument('[project-name]', 'Project name')
.option('-t, --template <template>', 'Project template (basic, phase6)', 'basic')
.option('-y, --yes', 'Skip prompts and use defaults')
.action(async (projectName, options) => {
try {
let finalProjectName = projectName;
let selectedTemplate = options?.template || 'basic';
// Interactive prompts if not using --yes flag
if (!options?.yes) {
const response = await prompts([
{
type: 'text',
name: 'projectName',
message: 'Project name:',
initial: projectName || 'my-tachui-app',
validate: (value) => (value.length > 0 ? true : 'Project name is required'),
},
{
type: 'select',
name: 'template',
message: 'Choose a template:',
choices: Object.entries(templates).map(([key, template]) => ({
title: template.name,
description: template.description,
value: key,
})),
initial: selectedTemplate === 'phase6' ? 1 : 0,
},
]);
if (!response.projectName) {
console.log(chalk.yellow('Operation cancelled'));
return;
}
finalProjectName = response.projectName;
selectedTemplate = response.template;
}
if (!finalProjectName) {
console.error(chalk.red('Project name is required'));
process.exit(1);
}
const template = templates[selectedTemplate];
if (!template) {
console.error(chalk.red(`Template "${selectedTemplate}" not found`));
process.exit(1);
}
const projectPath = resolve(finalProjectName);
// Check if directory already exists
if (existsSync(projectPath)) {
console.error(chalk.red(`Directory "${finalProjectName}" already exists`));
process.exit(1);
}
const spinner = ora('Creating TachUI project...').start();
// Create project directory
mkdirSync(projectPath, { recursive: true });
// Create all template files
for (const [filePath, content] of Object.entries(template.files)) {
const fullPath = join(projectPath, filePath);
const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
// Create directory if it doesn't exist
if (dir !== projectPath) {
mkdirSync(dir, { recursive: true });
}
// Replace template variables
const processedContent = content.replace(/{projectName}/g, finalProjectName);
writeFileSync(fullPath, processedContent);
}
spinner.succeed('TachUI project created successfully!');
// Success message with features
console.log(`
${chalk.green('✅ Project created:')} ${chalk.cyan(finalProjectName)}
${chalk.green('📁 Location:')} ${projectPath}
${chalk.green('🎨 Template:')} ${template.name}
${chalk.yellow('Features included:')}
${template.features.map((feature) => ` • ${feature}`).join('\n')}
${chalk.yellow('Next steps:')}
cd ${finalProjectName}
npm install
npm run dev
${chalk.green('Happy coding with TachUI! 🚀')}
`);
}
catch (error) {
console.error(chalk.red('Error creating project:'), error.message);
process.exit(1);
}
});
//# sourceMappingURL=init.js.map