@thinksoftai/cli 1.1.1 → 1.3.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.
@@ -0,0 +1,1241 @@
1
+ "use strict";
2
+ /**
3
+ * Dev command - AI-assisted frontend development with auto-setup
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.dev = dev;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const readline = __importStar(require("readline"));
46
+ const chalk_1 = __importDefault(require("chalk"));
47
+ const ora_1 = __importDefault(require("ora"));
48
+ const inquirer_1 = __importDefault(require("inquirer"));
49
+ const child_process_1 = require("child_process");
50
+ const config = __importStar(require("../utils/config"));
51
+ const logger = __importStar(require("../utils/logger"));
52
+ const api = __importStar(require("../utils/api"));
53
+ const BASE_URL = 'https://thinksoftai.com/api/v1';
54
+ async function dev(options) {
55
+ if (!config.isLoggedIn()) {
56
+ logger.error('Not logged in');
57
+ logger.info('Use "thinksoft login" to authenticate');
58
+ return;
59
+ }
60
+ // Check if project exists
61
+ const hasProject = fs.existsSync(path.join(process.cwd(), 'package.json'));
62
+ let projectConfig = config.getProjectConfig();
63
+ let appId = options.app || projectConfig?.appId;
64
+ let framework = projectConfig?.framework || 'react';
65
+ // --init flag: Generate frontend directly without requiring existing project
66
+ if (options.init) {
67
+ if (!appId) {
68
+ // Need to select an app first
69
+ const appsResult = await api.listApps();
70
+ if (appsResult.error || !appsResult.apps?.length) {
71
+ logger.error('No apps found. Create one first:');
72
+ logger.info('thinksoft create "Your app description"');
73
+ return;
74
+ }
75
+ const { selectedApp } = await inquirer_1.default.prompt([{
76
+ type: 'list',
77
+ name: 'selectedApp',
78
+ message: 'Select an app to generate frontend for:',
79
+ choices: appsResult.apps.map((a) => ({
80
+ name: `${a.name} (${a.short_id})`,
81
+ value: a.short_id
82
+ }))
83
+ }]);
84
+ appId = selectedApp;
85
+ }
86
+ showDevBanner(appId, framework);
87
+ await runInitWithSuggestion(appId, framework);
88
+ return;
89
+ }
90
+ // Auto-setup flow (for interactive dev mode)
91
+ if (!hasProject) {
92
+ console.log();
93
+ console.log(chalk_1.default.yellow('📁 No project detected in current directory.'));
94
+ console.log();
95
+ // Need appId for setup
96
+ if (!appId) {
97
+ // List user's apps and let them choose
98
+ const appsResult = await api.listApps();
99
+ if (appsResult.error || !appsResult.apps?.length) {
100
+ logger.error('No apps found. Create one first:');
101
+ logger.info('thinksoft create "Your app description"');
102
+ return;
103
+ }
104
+ const { selectedApp } = await inquirer_1.default.prompt([{
105
+ type: 'list',
106
+ name: 'selectedApp',
107
+ message: 'Select an app to develop:',
108
+ choices: appsResult.apps.map((a) => ({
109
+ name: `${a.name} (${a.short_id})`,
110
+ value: a.short_id
111
+ }))
112
+ }]);
113
+ appId = selectedApp;
114
+ }
115
+ // Ask to create project
116
+ const { createProject } = await inquirer_1.default.prompt([{
117
+ type: 'confirm',
118
+ name: 'createProject',
119
+ message: 'Create a new project?',
120
+ default: true
121
+ }]);
122
+ if (!createProject) {
123
+ logger.info('Run "thinksoft dev" in an existing project directory');
124
+ return;
125
+ }
126
+ // Select framework
127
+ const { selectedFramework } = await inquirer_1.default.prompt([{
128
+ type: 'list',
129
+ name: 'selectedFramework',
130
+ message: 'Select framework:',
131
+ choices: [
132
+ { name: 'React (Vite)', value: 'react' },
133
+ { name: 'Next.js', value: 'nextjs' },
134
+ { name: 'Vue (Vite)', value: 'vue' }
135
+ ]
136
+ }]);
137
+ framework = selectedFramework;
138
+ // Get project name
139
+ const defaultName = (appId || 'my-app').toLowerCase().replace(/[^a-z0-9]/g, '-') + '-app';
140
+ const { projectName } = await inquirer_1.default.prompt([{
141
+ type: 'input',
142
+ name: 'projectName',
143
+ message: 'Project name:',
144
+ default: defaultName
145
+ }]);
146
+ // Run setup
147
+ const setupSuccess = await setupProject(projectName, framework, appId);
148
+ if (!setupSuccess) {
149
+ return;
150
+ }
151
+ // Update projectConfig after setup
152
+ projectConfig = config.getProjectConfig();
153
+ }
154
+ else if (!projectConfig) {
155
+ // Has package.json but no thinksoft.json
156
+ console.log();
157
+ console.log(chalk_1.default.yellow('📋 Existing project found, but not linked to ThinkSoft.'));
158
+ console.log();
159
+ if (!appId) {
160
+ // List user's apps
161
+ const appsResult = await api.listApps();
162
+ if (appsResult.error || !appsResult.apps?.length) {
163
+ logger.error('No apps found. Create one first:');
164
+ logger.info('thinksoft create "Your app description"');
165
+ return;
166
+ }
167
+ const { selectedApp } = await inquirer_1.default.prompt([{
168
+ type: 'list',
169
+ name: 'selectedApp',
170
+ message: 'Select an app to link:',
171
+ choices: appsResult.apps.map((a) => ({
172
+ name: `${a.name} (${a.short_id})`,
173
+ value: a.short_id
174
+ }))
175
+ }]);
176
+ appId = selectedApp;
177
+ }
178
+ // Detect framework from package.json
179
+ framework = detectFramework();
180
+ // Create thinksoft.json
181
+ config.saveProjectConfig({
182
+ appId: appId,
183
+ name: appId,
184
+ framework,
185
+ outputDir: framework === 'nextjs' ? '.next' : 'dist'
186
+ });
187
+ logger.success('Project linked to ThinkSoft');
188
+ // Create SDK client file if not exists
189
+ createSdkClientFile(appId, framework);
190
+ // Install SDK if not installed
191
+ await installSdkIfNeeded();
192
+ projectConfig = config.getProjectConfig();
193
+ }
194
+ if (!appId) {
195
+ logger.error('No app ID found');
196
+ return;
197
+ }
198
+ // Show dev banner
199
+ showDevBanner(appId, framework);
200
+ // If --init flag, run suggestion-based initialization
201
+ if (options.init) {
202
+ await runInitWithSuggestion(appId, framework);
203
+ return;
204
+ }
205
+ // Start interactive prompt
206
+ await runDevPrompt(appId, framework);
207
+ }
208
+ /**
209
+ * Setup a new project with selected framework
210
+ */
211
+ async function setupProject(name, framework, appId) {
212
+ console.log();
213
+ // Step 1: Create project
214
+ const createSpinner = (0, ora_1.default)(`Creating ${framework} project...`).start();
215
+ try {
216
+ switch (framework) {
217
+ case 'react':
218
+ (0, child_process_1.execSync)(`npm create vite@latest ${name} -- --template react-ts`, {
219
+ stdio: 'pipe',
220
+ cwd: process.cwd()
221
+ });
222
+ break;
223
+ case 'nextjs':
224
+ (0, child_process_1.execSync)(`npx create-next-app@latest ${name} --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --no-git`, {
225
+ stdio: 'pipe',
226
+ cwd: process.cwd()
227
+ });
228
+ break;
229
+ case 'vue':
230
+ (0, child_process_1.execSync)(`npm create vite@latest ${name} -- --template vue-ts`, {
231
+ stdio: 'pipe',
232
+ cwd: process.cwd()
233
+ });
234
+ break;
235
+ }
236
+ createSpinner.succeed('Project created');
237
+ }
238
+ catch (err) {
239
+ createSpinner.fail('Failed to create project');
240
+ console.log(chalk_1.default.red(err.message));
241
+ return false;
242
+ }
243
+ // Change to project directory
244
+ const projectPath = path.join(process.cwd(), name);
245
+ process.chdir(projectPath);
246
+ // Step 2: Install dependencies
247
+ const installSpinner = (0, ora_1.default)('Installing dependencies...').start();
248
+ try {
249
+ (0, child_process_1.execSync)('npm install', { stdio: 'pipe' });
250
+ (0, child_process_1.execSync)('npm install @thinksoftai/sdk', { stdio: 'pipe' });
251
+ // Install Tailwind for React/Vue (Next.js includes it)
252
+ if (framework === 'react' || framework === 'vue') {
253
+ (0, child_process_1.execSync)('npm install -D tailwindcss postcss autoprefixer', { stdio: 'pipe' });
254
+ (0, child_process_1.execSync)('npx tailwindcss init -p', { stdio: 'pipe' });
255
+ }
256
+ installSpinner.succeed('@thinksoftai/sdk installed');
257
+ }
258
+ catch (err) {
259
+ installSpinner.fail('Failed to install dependencies');
260
+ console.log(chalk_1.default.red(err.message));
261
+ return false;
262
+ }
263
+ // Step 3: Configure project
264
+ const configSpinner = (0, ora_1.default)('Configuring project...').start();
265
+ try {
266
+ // Create thinksoft.json
267
+ config.saveProjectConfig({
268
+ appId,
269
+ name,
270
+ framework,
271
+ outputDir: framework === 'nextjs' ? '.next' : 'dist'
272
+ });
273
+ // Create SDK client file
274
+ createSdkClientFile(appId, framework);
275
+ // Create basic App shell (for React/Vue)
276
+ if (framework === 'react') {
277
+ createReactAppShell(name);
278
+ }
279
+ configSpinner.succeed('Project configured');
280
+ }
281
+ catch (err) {
282
+ configSpinner.fail('Failed to configure project');
283
+ console.log(chalk_1.default.red(err.message));
284
+ return false;
285
+ }
286
+ console.log();
287
+ logger.success(`Project "${name}" ready!`);
288
+ console.log();
289
+ return true;
290
+ }
291
+ /**
292
+ * Detect framework from existing package.json
293
+ */
294
+ function detectFramework() {
295
+ try {
296
+ const pkgPath = path.join(process.cwd(), 'package.json');
297
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
298
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
299
+ if (deps['next'])
300
+ return 'nextjs';
301
+ if (deps['vue'])
302
+ return 'vue';
303
+ return 'react';
304
+ }
305
+ catch {
306
+ return 'react';
307
+ }
308
+ }
309
+ /**
310
+ * Install SDK if not already installed
311
+ */
312
+ async function installSdkIfNeeded() {
313
+ try {
314
+ const pkgPath = path.join(process.cwd(), 'package.json');
315
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
316
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
317
+ if (!deps['@thinksoftai/sdk']) {
318
+ const spinner = (0, ora_1.default)('Installing @thinksoftai/sdk...').start();
319
+ try {
320
+ (0, child_process_1.execSync)('npm install @thinksoftai/sdk', { stdio: 'pipe' });
321
+ spinner.succeed('@thinksoftai/sdk installed');
322
+ }
323
+ catch (err) {
324
+ spinner.fail('Failed to install SDK');
325
+ }
326
+ }
327
+ }
328
+ catch {
329
+ // Ignore errors
330
+ }
331
+ }
332
+ /**
333
+ * Create SDK client file
334
+ */
335
+ function createSdkClientFile(appId, framework) {
336
+ const libDir = framework === 'nextjs'
337
+ ? path.join(process.cwd(), 'lib')
338
+ : path.join(process.cwd(), 'src', 'lib');
339
+ if (!fs.existsSync(libDir)) {
340
+ fs.mkdirSync(libDir, { recursive: true });
341
+ }
342
+ const clientPath = path.join(libDir, 'thinksoft.ts');
343
+ if (!fs.existsSync(clientPath)) {
344
+ const content = `import { ThinkSoft } from '@thinksoftai/sdk'
345
+
346
+ // Initialize ThinkSoft client
347
+ export const client = new ThinkSoft({
348
+ appId: '${appId}'
349
+ })
350
+
351
+ // Re-export for convenience
352
+ export { ThinkSoft }
353
+ `;
354
+ fs.writeFileSync(clientPath, content);
355
+ console.log(chalk_1.default.green(` ✔ ${framework === 'nextjs' ? 'lib' : 'src/lib'}/thinksoft.ts created`));
356
+ }
357
+ }
358
+ /**
359
+ * Create basic React App shell with Tailwind
360
+ */
361
+ function createReactAppShell(name) {
362
+ const appPath = path.join(process.cwd(), 'src', 'App.tsx');
363
+ const content = `import './index.css'
364
+
365
+ function App() {
366
+ return (
367
+ <div className="min-h-screen bg-gray-100">
368
+ <header className="bg-white shadow">
369
+ <div className="max-w-7xl mx-auto py-6 px-4">
370
+ <h1 className="text-3xl font-bold text-gray-900">
371
+ ${name}
372
+ </h1>
373
+ </div>
374
+ </header>
375
+ <main className="max-w-7xl mx-auto py-6 px-4">
376
+ {/* Your components will be generated here */}
377
+ <p className="text-gray-600">
378
+ Use <code className="bg-gray-200 px-2 py-1 rounded">thinksoft dev</code> to start building!
379
+ </p>
380
+ </main>
381
+ </div>
382
+ )
383
+ }
384
+
385
+ export default App
386
+ `;
387
+ fs.writeFileSync(appPath, content);
388
+ // Update Tailwind config
389
+ const tailwindConfig = `/** @type {import('tailwindcss').Config} */
390
+ export default {
391
+ content: [
392
+ "./index.html",
393
+ "./src/**/*.{js,ts,jsx,tsx}",
394
+ ],
395
+ theme: {
396
+ extend: {},
397
+ },
398
+ plugins: [],
399
+ }
400
+ `;
401
+ fs.writeFileSync(path.join(process.cwd(), 'tailwind.config.js'), tailwindConfig);
402
+ // Update index.css with Tailwind directives
403
+ const indexCss = `@tailwind base;
404
+ @tailwind components;
405
+ @tailwind utilities;
406
+ `;
407
+ fs.writeFileSync(path.join(process.cwd(), 'src', 'index.css'), indexCss);
408
+ }
409
+ function showDevBanner(appId, framework) {
410
+ console.log();
411
+ console.log(chalk_1.default.magenta('╔═══════════════════════════════════════════════════════╗'));
412
+ console.log(chalk_1.default.magenta('║') + chalk_1.default.white.bold(' ThinkSoft Dev - AI-Assisted Development ') + chalk_1.default.magenta('║'));
413
+ console.log(chalk_1.default.magenta('║') + chalk_1.default.gray(` App: ${appId} | Framework: ${framework}`.padEnd(55)) + chalk_1.default.magenta('║'));
414
+ console.log(chalk_1.default.magenta('╠═══════════════════════════════════════════════════════╣'));
415
+ console.log(chalk_1.default.magenta('║') + chalk_1.default.gray(' Describe what you want to build. Type ') + chalk_1.default.yellow('help') + chalk_1.default.gray(' or ') + chalk_1.default.yellow('quit') + chalk_1.default.gray(' ') + chalk_1.default.magenta('║'));
416
+ console.log(chalk_1.default.magenta('╚═══════════════════════════════════════════════════════╝'));
417
+ console.log();
418
+ }
419
+ function showDevHelp() {
420
+ console.log();
421
+ console.log(chalk_1.default.cyan.bold('ThinkSoft Dev Commands:'));
422
+ console.log();
423
+ console.log(chalk_1.default.yellow(' <prompt>') + chalk_1.default.white(' Describe what you want to build'));
424
+ console.log(chalk_1.default.yellow(' help') + chalk_1.default.white(' Show this help message'));
425
+ console.log(chalk_1.default.yellow(' files') + chalk_1.default.white(' List files in src/ directory'));
426
+ console.log(chalk_1.default.yellow(' clear') + chalk_1.default.white(' Clear screen'));
427
+ console.log(chalk_1.default.yellow(' quit / exit') + chalk_1.default.white(' Exit dev mode'));
428
+ console.log();
429
+ console.log(chalk_1.default.gray('Examples:'));
430
+ console.log(chalk_1.default.gray(' dev> Create a dashboard showing all leads with status filters'));
431
+ console.log(chalk_1.default.gray(' dev> Add a form to create new contacts'));
432
+ console.log(chalk_1.default.gray(' dev> Update LeadsPage to include a search bar'));
433
+ console.log();
434
+ }
435
+ async function runDevPrompt(appId, framework) {
436
+ let isRunning = true;
437
+ const createPrompt = () => {
438
+ if (!isRunning)
439
+ return;
440
+ const rl = readline.createInterface({
441
+ input: process.stdin,
442
+ output: process.stdout
443
+ });
444
+ rl.question(chalk_1.default.magenta('dev> '), async (input) => {
445
+ rl.close();
446
+ const trimmed = input.trim();
447
+ if (!trimmed) {
448
+ setTimeout(() => createPrompt(), 50);
449
+ return;
450
+ }
451
+ const command = trimmed.toLowerCase();
452
+ if (command === 'quit' || command === 'exit') {
453
+ console.log(chalk_1.default.cyan('\nGoodbye! Run "npm run build && thinksoft deploy" when ready.\n'));
454
+ isRunning = false;
455
+ return;
456
+ }
457
+ if (command === 'help') {
458
+ showDevHelp();
459
+ setTimeout(() => createPrompt(), 50);
460
+ return;
461
+ }
462
+ if (command === 'files') {
463
+ listSrcFiles();
464
+ setTimeout(() => createPrompt(), 50);
465
+ return;
466
+ }
467
+ if (command === 'clear') {
468
+ console.clear();
469
+ showDevBanner(appId, framework);
470
+ setTimeout(() => createPrompt(), 50);
471
+ return;
472
+ }
473
+ // Generate code
474
+ await generateCode(appId, framework, trimmed);
475
+ setTimeout(() => createPrompt(), 50);
476
+ });
477
+ };
478
+ createPrompt();
479
+ }
480
+ function listSrcFiles() {
481
+ const srcPath = path.join(process.cwd(), 'src');
482
+ if (!fs.existsSync(srcPath)) {
483
+ console.log(chalk_1.default.yellow('\nNo src/ directory found\n'));
484
+ return;
485
+ }
486
+ console.log(chalk_1.default.cyan('\nFiles in src/:'));
487
+ listFilesRecursive(srcPath, 0);
488
+ console.log();
489
+ }
490
+ function listFilesRecursive(dirPath, depth) {
491
+ const items = fs.readdirSync(dirPath);
492
+ for (const item of items) {
493
+ const fullPath = path.join(dirPath, item);
494
+ const stat = fs.statSync(fullPath);
495
+ const indent = ' '.repeat(depth + 1);
496
+ if (stat.isDirectory()) {
497
+ console.log(chalk_1.default.blue(`${indent}📁 ${item}/`));
498
+ listFilesRecursive(fullPath, depth + 1);
499
+ }
500
+ else {
501
+ console.log(chalk_1.default.gray(`${indent}📄 ${item}`));
502
+ }
503
+ }
504
+ }
505
+ /**
506
+ * Analyze user prompt and return implementation plan
507
+ */
508
+ async function analyzePrompt(appId, userPrompt, existingFiles) {
509
+ const token = config.getToken();
510
+ if (!token) {
511
+ return null;
512
+ }
513
+ try {
514
+ const response = await fetch(`${BASE_URL}/dev/analyze`, {
515
+ method: 'POST',
516
+ headers: {
517
+ 'Authorization': `Bearer ${token}`,
518
+ 'Content-Type': 'application/json'
519
+ },
520
+ body: JSON.stringify({
521
+ appId,
522
+ prompt: userPrompt,
523
+ existingFiles: existingFiles.map(f => ({ path: f.path })) // Only send paths for analyze
524
+ })
525
+ });
526
+ if (!response.ok) {
527
+ return null;
528
+ }
529
+ const result = await response.json();
530
+ return result.plan || null;
531
+ }
532
+ catch {
533
+ return null;
534
+ }
535
+ }
536
+ /**
537
+ * Display the implementation plan
538
+ */
539
+ function showPlan(plan) {
540
+ console.log();
541
+ console.log(chalk_1.default.cyan('┌─────────────────────────────────────────────────────────┐'));
542
+ console.log(chalk_1.default.cyan('│') + chalk_1.default.white.bold(' Plan ') + chalk_1.default.cyan('│'));
543
+ console.log(chalk_1.default.cyan('└─────────────────────────────────────────────────────────┘'));
544
+ console.log();
545
+ // Summary
546
+ console.log(chalk_1.default.white(' ' + plan.summary));
547
+ console.log();
548
+ // Tables
549
+ if (plan.tables_needed && plan.tables_needed.length > 0) {
550
+ console.log(chalk_1.default.gray(' Tables: ') + chalk_1.default.yellow(plan.tables_needed.join(', ')));
551
+ console.log();
552
+ }
553
+ // Files
554
+ console.log(chalk_1.default.gray(' Files:'));
555
+ for (const file of plan.files) {
556
+ const icon = file.action === 'create' ? '✨' : '📝';
557
+ const actionColor = file.action === 'create' ? chalk_1.default.green : chalk_1.default.blue;
558
+ console.log(` ${icon} ${actionColor(file.path)}`);
559
+ console.log(chalk_1.default.gray(` ${file.purpose}`));
560
+ }
561
+ // Questions (if any)
562
+ if (plan.questions && plan.questions.length > 0) {
563
+ console.log();
564
+ console.log(chalk_1.default.yellow(' ⚠️ Questions:'));
565
+ for (const q of plan.questions) {
566
+ console.log(chalk_1.default.yellow(` • ${q}`));
567
+ }
568
+ }
569
+ console.log();
570
+ }
571
+ /**
572
+ * Main flow: Analyze prompt, show plan, generate code
573
+ */
574
+ async function handleUserPrompt(appId, framework, userPrompt) {
575
+ console.log();
576
+ // Step 1: Analyze prompt
577
+ const analyzeSpinner = (0, ora_1.default)('Analyzing request...').start();
578
+ const existingFiles = collectExistingFiles();
579
+ const plan = await analyzePrompt(appId, userPrompt, existingFiles);
580
+ if (!plan) {
581
+ analyzeSpinner.text = 'Generating code...';
582
+ // Fallback to direct generation if analyze fails
583
+ await generateCodeDirect(appId, framework, userPrompt, existingFiles, null, analyzeSpinner);
584
+ return;
585
+ }
586
+ analyzeSpinner.succeed('Analysis complete');
587
+ // Step 2: Show plan
588
+ showPlan(plan);
589
+ // Step 3: Confirm if complex
590
+ let proceed = true;
591
+ if (plan.complexity === 'complex') {
592
+ const { confirmed } = await inquirer_1.default.prompt([{
593
+ type: 'confirm',
594
+ name: 'confirmed',
595
+ message: 'Proceed with this plan?',
596
+ default: true
597
+ }]);
598
+ proceed = confirmed;
599
+ }
600
+ if (!proceed) {
601
+ console.log(chalk_1.default.yellow(' Cancelled. Try rephrasing your request.'));
602
+ console.log();
603
+ return;
604
+ }
605
+ // Step 4: Generate code with plan
606
+ const generateSpinner = (0, ora_1.default)('Generating code...').start();
607
+ await generateCodeDirect(appId, framework, userPrompt, existingFiles, plan, generateSpinner);
608
+ }
609
+ /**
610
+ * Generate code (internal function used by handleUserPrompt)
611
+ */
612
+ async function generateCodeDirect(appId, framework, userPrompt, existingFiles, plan, spinner) {
613
+ try {
614
+ const token = config.getToken();
615
+ if (!token) {
616
+ spinner.fail('Not authenticated');
617
+ return;
618
+ }
619
+ spinner.text = `Generating code (${existingFiles.length} files for context)...`;
620
+ // Make request to Firebase Function
621
+ const response = await fetch(`${BASE_URL}/dev/generate`, {
622
+ method: 'POST',
623
+ headers: {
624
+ 'Authorization': `Bearer ${token}`,
625
+ 'Content-Type': 'application/json'
626
+ },
627
+ body: JSON.stringify({
628
+ appId,
629
+ prompt: userPrompt,
630
+ framework,
631
+ existingFiles,
632
+ plan // Include plan for better generation
633
+ })
634
+ });
635
+ if (!response.ok) {
636
+ const errorData = await response.json().catch(() => ({}));
637
+ spinner.fail('Generation failed');
638
+ console.log(chalk_1.default.red(`Error: ${errorData.error || `HTTP ${response.status}`}`));
639
+ return;
640
+ }
641
+ const result = await response.json();
642
+ if (!result.success || !result.files || result.files.length === 0) {
643
+ spinner.fail('No files generated');
644
+ if (result.error) {
645
+ console.log(chalk_1.default.red(`Error: ${result.error}`));
646
+ }
647
+ return;
648
+ }
649
+ spinner.succeed(`Generated ${result.files.length} file(s)`);
650
+ console.log();
651
+ // Write files
652
+ for (const file of result.files) {
653
+ writeGeneratedFile(file);
654
+ }
655
+ // Install dependencies if any
656
+ if (result.dependencies && result.dependencies.length > 0) {
657
+ console.log();
658
+ await installDependencies(result.dependencies);
659
+ }
660
+ console.log();
661
+ console.log(chalk_1.default.gray(`Tokens used: ${result.tokensUsed || 'N/A'}`));
662
+ console.log();
663
+ }
664
+ catch (err) {
665
+ spinner.fail('Generation failed');
666
+ console.log(chalk_1.default.red(`Error: ${err.message}`));
667
+ }
668
+ }
669
+ /**
670
+ * Legacy generateCode function (for backwards compatibility)
671
+ */
672
+ async function generateCode(appId, framework, userPrompt) {
673
+ await handleUserPrompt(appId, framework, userPrompt);
674
+ }
675
+ function collectExistingFiles() {
676
+ const files = [];
677
+ const srcPath = path.join(process.cwd(), 'src');
678
+ if (!fs.existsSync(srcPath)) {
679
+ return files;
680
+ }
681
+ // Collect relevant files (tsx, ts, jsx, js)
682
+ collectFilesRecursive(srcPath, 'src', files);
683
+ // Limit to reasonable context size (first 20 files, max 500 lines each)
684
+ return files.slice(0, 20).map(f => ({
685
+ path: f.path,
686
+ content: f.content.split('\n').slice(0, 500).join('\n')
687
+ }));
688
+ }
689
+ function collectFilesRecursive(dirPath, prefix, files) {
690
+ const items = fs.readdirSync(dirPath);
691
+ for (const item of items) {
692
+ const fullPath = path.join(dirPath, item);
693
+ const relativePath = `${prefix}/${item}`;
694
+ const stat = fs.statSync(fullPath);
695
+ if (stat.isDirectory()) {
696
+ // Skip node_modules and hidden directories
697
+ if (item !== 'node_modules' && !item.startsWith('.')) {
698
+ collectFilesRecursive(fullPath, relativePath, files);
699
+ }
700
+ }
701
+ else {
702
+ // Include TypeScript/JavaScript files
703
+ if (/\.(tsx?|jsx?)$/.test(item)) {
704
+ try {
705
+ const content = fs.readFileSync(fullPath, 'utf-8');
706
+ files.push({ path: relativePath, content });
707
+ }
708
+ catch {
709
+ // Skip unreadable files
710
+ }
711
+ }
712
+ }
713
+ }
714
+ }
715
+ // Track current project directory for file operations
716
+ let projectBaseDir = process.cwd();
717
+ function setProjectBaseDir(dir) {
718
+ projectBaseDir = dir;
719
+ }
720
+ function getProjectBaseDir() {
721
+ return projectBaseDir;
722
+ }
723
+ function writeGeneratedFile(file) {
724
+ const fullPath = path.join(projectBaseDir, file.path);
725
+ const dir = path.dirname(fullPath);
726
+ // Create directory if needed
727
+ if (!fs.existsSync(dir)) {
728
+ fs.mkdirSync(dir, { recursive: true });
729
+ }
730
+ // Check if file exists
731
+ const exists = fs.existsSync(fullPath);
732
+ const action = exists ? 'Updated' : 'Created';
733
+ // Write file
734
+ fs.writeFileSync(fullPath, file.content);
735
+ const icon = exists ? '📝' : '✨';
736
+ console.log(chalk_1.default.green(`${icon} ${action}: ${file.path}`));
737
+ }
738
+ /**
739
+ * Run initialization with AI-generated suggestion
740
+ */
741
+ async function runInitWithSuggestion(appId, framework) {
742
+ console.log();
743
+ console.log(chalk_1.default.cyan('🧠 Analyzing your app to suggest a purpose-built frontend...'));
744
+ console.log();
745
+ const spinner = (0, ora_1.default)('Generating intelligent suggestion...').start();
746
+ // Fetch suggestion from API
747
+ const suggestion = await fetchFrontendSuggestion(appId);
748
+ if (!suggestion) {
749
+ spinner.fail('Failed to generate suggestion');
750
+ console.log(chalk_1.default.yellow(' Try running without --init flag for manual development'));
751
+ return;
752
+ }
753
+ spinner.succeed('Analysis complete!');
754
+ console.log();
755
+ // Display reasoning
756
+ displaySuggestionReasoning(suggestion);
757
+ // Display suggested frontend apps
758
+ displaySuggestedApps(suggestion);
759
+ // Ask user to confirm
760
+ const { proceed } = await inquirer_1.default.prompt([{
761
+ type: 'confirm',
762
+ name: 'proceed',
763
+ message: 'Generate this frontend?',
764
+ default: true
765
+ }]);
766
+ if (!proceed) {
767
+ console.log(chalk_1.default.yellow('\n Cancelled. Run "thinksoft dev" for manual development.\n'));
768
+ return;
769
+ }
770
+ // Let user select which app to generate (if multiple)
771
+ let selectedApp;
772
+ let projectDir = '';
773
+ if (suggestion.frontend_apps.length > 1) {
774
+ const { appChoice } = await inquirer_1.default.prompt([{
775
+ type: 'list',
776
+ name: 'appChoice',
777
+ message: 'Which frontend app to generate?',
778
+ choices: [
779
+ ...suggestion.frontend_apps.map(app => ({
780
+ name: `${app.name} (${app.audience})`,
781
+ value: app.slug
782
+ })),
783
+ { name: 'All apps', value: '__all__' }
784
+ ]
785
+ }]);
786
+ if (appChoice === '__all__') {
787
+ // Generate all apps
788
+ const projectDirs = [];
789
+ for (const app of suggestion.frontend_apps) {
790
+ const dir = await generateFrontendApp(appId, framework, app, suggestion.shared_components);
791
+ projectDirs.push(dir);
792
+ }
793
+ // Show summary for all generated apps
794
+ console.log();
795
+ console.log(chalk_1.default.green('✨ All frontend apps generated successfully!'));
796
+ console.log();
797
+ console.log(chalk_1.default.gray('Generated projects:'));
798
+ for (const dir of projectDirs) {
799
+ const dirName = path.basename(dir);
800
+ console.log(chalk_1.default.gray(` • ${dirName}/`));
801
+ }
802
+ console.log();
803
+ console.log(chalk_1.default.gray('Next steps for each project:'));
804
+ console.log(chalk_1.default.white(' 1. cd <project-folder>'));
805
+ console.log(chalk_1.default.white(' 2. npm install'));
806
+ console.log(chalk_1.default.white(' 3. npm run dev'));
807
+ console.log();
808
+ return;
809
+ }
810
+ else {
811
+ selectedApp = suggestion.frontend_apps.find(a => a.slug === appChoice);
812
+ projectDir = await generateFrontendApp(appId, framework, selectedApp, suggestion.shared_components);
813
+ }
814
+ }
815
+ else {
816
+ selectedApp = suggestion.frontend_apps[0];
817
+ projectDir = await generateFrontendApp(appId, framework, selectedApp, suggestion.shared_components);
818
+ }
819
+ const projectName = path.basename(projectDir);
820
+ console.log();
821
+ console.log(chalk_1.default.green('✨ Frontend generated successfully!'));
822
+ console.log();
823
+ console.log(chalk_1.default.cyan('┌─────────────────────────────────────────────────────────────┐'));
824
+ console.log(chalk_1.default.cyan('│') + chalk_1.default.white.bold(' Next Steps ') + chalk_1.default.cyan('│'));
825
+ console.log(chalk_1.default.cyan('└─────────────────────────────────────────────────────────────┘'));
826
+ console.log();
827
+ console.log(chalk_1.default.white(` 1. cd ${projectName}`));
828
+ console.log(chalk_1.default.white(' 2. npm install'));
829
+ console.log(chalk_1.default.white(' 3. npm run dev'));
830
+ console.log();
831
+ console.log(chalk_1.default.gray(' To deploy:'));
832
+ console.log(chalk_1.default.white(' 4. npm run build'));
833
+ console.log(chalk_1.default.white(' 5. npm run deploy'));
834
+ console.log();
835
+ }
836
+ /**
837
+ * Fetch frontend suggestion from API
838
+ */
839
+ async function fetchFrontendSuggestion(appId) {
840
+ const token = config.getToken();
841
+ if (!token) {
842
+ return null;
843
+ }
844
+ try {
845
+ const response = await fetch(`${BASE_URL}/dev/suggest`, {
846
+ method: 'POST',
847
+ headers: {
848
+ 'Authorization': `Bearer ${token}`,
849
+ 'Content-Type': 'application/json'
850
+ },
851
+ body: JSON.stringify({ appId })
852
+ });
853
+ if (!response.ok) {
854
+ return null;
855
+ }
856
+ const result = await response.json();
857
+ return result.suggestion || null;
858
+ }
859
+ catch {
860
+ return null;
861
+ }
862
+ }
863
+ /**
864
+ * Display reasoning analysis
865
+ */
866
+ function displaySuggestionReasoning(suggestion) {
867
+ const reasoning = suggestion.reasoning;
868
+ console.log(chalk_1.default.cyan('┌─────────────────────────────────────────────────────────────┐'));
869
+ console.log(chalk_1.default.cyan('│') + chalk_1.default.white.bold(' AI Analysis ') + chalk_1.default.cyan('│'));
870
+ console.log(chalk_1.default.cyan('└─────────────────────────────────────────────────────────────┘'));
871
+ console.log();
872
+ // Purpose
873
+ console.log(chalk_1.default.white.bold(' Purpose:'));
874
+ console.log(chalk_1.default.gray(` ${reasoning.inferred_purpose}`));
875
+ console.log();
876
+ // Target Users
877
+ console.log(chalk_1.default.white.bold(' Target Users:'));
878
+ for (const user of reasoning.target_users) {
879
+ const typeLabel = user.type === 'primary' ? chalk_1.default.green('●') : chalk_1.default.gray('○');
880
+ console.log(` ${typeLabel} ${chalk_1.default.white(user.who)}`);
881
+ console.log(chalk_1.default.gray(` Needs: ${user.needs}`));
882
+ }
883
+ console.log();
884
+ // Key Workflows
885
+ console.log(chalk_1.default.white.bold(' Key Workflows:'));
886
+ for (const workflow of reasoning.key_workflows) {
887
+ console.log(chalk_1.default.gray(` → ${workflow}`));
888
+ }
889
+ console.log();
890
+ // UI Approach
891
+ console.log(chalk_1.default.white.bold(' UI Approach:'));
892
+ console.log(chalk_1.default.gray(` ${reasoning.ui_approach}`));
893
+ console.log();
894
+ }
895
+ /**
896
+ * Display suggested frontend apps
897
+ */
898
+ function displaySuggestedApps(suggestion) {
899
+ console.log(chalk_1.default.cyan('┌─────────────────────────────────────────────────────────────┐'));
900
+ console.log(chalk_1.default.cyan('│') + chalk_1.default.white.bold(' Suggested Frontend ') + chalk_1.default.cyan('│'));
901
+ console.log(chalk_1.default.cyan('└─────────────────────────────────────────────────────────────┘'));
902
+ console.log();
903
+ for (const app of suggestion.frontend_apps) {
904
+ console.log(chalk_1.default.yellow.bold(` 📱 ${app.name}`));
905
+ console.log(chalk_1.default.gray(` ${app.description}`));
906
+ console.log(chalk_1.default.gray(` Audience: ${app.audience}`));
907
+ console.log();
908
+ console.log(chalk_1.default.white(' Pages:'));
909
+ for (const page of app.pages.slice(0, 6)) {
910
+ console.log(chalk_1.default.green(` ✦ ${page.name}`));
911
+ console.log(chalk_1.default.gray(` ${page.purpose}`));
912
+ if (page.key_features && page.key_features.length > 0) {
913
+ console.log(chalk_1.default.gray(` Features: ${page.key_features.join(', ')}`));
914
+ }
915
+ }
916
+ if (app.pages.length > 6) {
917
+ console.log(chalk_1.default.gray(` ... and ${app.pages.length - 6} more pages`));
918
+ }
919
+ console.log();
920
+ }
921
+ // Summary
922
+ console.log(chalk_1.default.white.bold(' Summary:'));
923
+ console.log(chalk_1.default.gray(` ${suggestion.summary}`));
924
+ console.log();
925
+ // Dependencies
926
+ if (suggestion.dependencies && suggestion.dependencies.length > 0) {
927
+ console.log(chalk_1.default.white.bold(' Dependencies:'));
928
+ console.log(chalk_1.default.gray(` ${suggestion.dependencies.join(', ')}`));
929
+ console.log();
930
+ }
931
+ }
932
+ /**
933
+ * Generate a frontend app from suggestion
934
+ */
935
+ async function generateFrontendApp(appId, framework, app, sharedComponents) {
936
+ // Create project directory based on app slug
937
+ const projectDir = path.join(process.cwd(), app.slug);
938
+ console.log();
939
+ console.log(chalk_1.default.cyan(`📦 Creating project: ${chalk_1.default.white.bold(app.slug)}/`));
940
+ console.log(chalk_1.default.gray(` ${app.name}`));
941
+ console.log();
942
+ // Create directory if it doesn't exist
943
+ if (!fs.existsSync(projectDir)) {
944
+ fs.mkdirSync(projectDir, { recursive: true });
945
+ }
946
+ // Set the project base directory for all file operations
947
+ setProjectBaseDir(projectDir);
948
+ // Initialize project with package.json
949
+ await initializeProject(projectDir, app.name, appId, framework);
950
+ // Build a comprehensive prompt for code generation
951
+ const pagesDescription = app.pages.map(p => `- ${p.name} (${p.path}): ${p.purpose}. Features: ${p.key_features.join(', ')}`).join('\n');
952
+ const componentsDescription = [
953
+ ...app.components.map(c => `- ${c.name}: ${c.purpose}`),
954
+ ...sharedComponents.map(c => `- ${c.name}: ${c.purpose}`)
955
+ ].join('\n');
956
+ const navDescription = app.navigation.map(n => `- ${n.label} → ${n.path}`).join('\n');
957
+ const prompt = `Generate a complete ${app.name} frontend with the following specifications:
958
+
959
+ ## App Purpose
960
+ ${app.description}
961
+ Audience: ${app.audience}
962
+
963
+ ## Pages to Generate
964
+ ${pagesDescription}
965
+
966
+ ## Components Needed
967
+ ${componentsDescription}
968
+
969
+ ## Navigation Structure
970
+ ${navDescription}
971
+
972
+ ## Requirements
973
+ - Create ALL pages listed above as complete, working files
974
+ - Use ThinkSoft SDK for all data operations
975
+ - Include proper TypeScript types
976
+ - Use Tailwind CSS for styling
977
+ - Handle loading, error, and empty states
978
+ - Make it responsive and mobile-friendly
979
+ - Generate the navigation/sidebar component
980
+ - Generate an App.tsx that sets up routing
981
+
982
+ Generate production-ready code for all files.`;
983
+ // Use the existing generateCode flow with this comprehensive prompt
984
+ const existingFiles = collectExistingFiles();
985
+ // Create a plan from the suggestion
986
+ const plan = {
987
+ complexity: 'complex',
988
+ intent: 'build_feature',
989
+ summary: app.description,
990
+ tables_needed: [...new Set(app.pages.flatMap(p => p.tables))],
991
+ files: [
992
+ ...app.pages.map(p => ({
993
+ path: p.path,
994
+ action: 'create',
995
+ purpose: p.purpose
996
+ })),
997
+ ...app.components.map(c => ({
998
+ path: c.path || `src/components/${c.name}.tsx`,
999
+ action: 'create',
1000
+ purpose: c.purpose
1001
+ })),
1002
+ ...sharedComponents.map(c => ({
1003
+ path: c.path || `src/components/${c.name}.tsx`,
1004
+ action: 'create',
1005
+ purpose: c.purpose
1006
+ })),
1007
+ { path: 'src/App.tsx', action: 'create', purpose: 'Main app with routing' },
1008
+ { path: 'src/components/Sidebar.tsx', action: 'create', purpose: 'Navigation sidebar' }
1009
+ ]
1010
+ };
1011
+ const spinner = (0, ora_1.default)('Generating code...').start();
1012
+ await generateCodeDirect(appId, framework, prompt, existingFiles, plan, spinner);
1013
+ // Reset base dir to current working directory
1014
+ setProjectBaseDir(process.cwd());
1015
+ return projectDir;
1016
+ }
1017
+ /**
1018
+ * Initialize a new project with package.json and config files
1019
+ */
1020
+ async function initializeProject(projectDir, appName, appId, framework) {
1021
+ const pkgPath = path.join(projectDir, 'package.json');
1022
+ // Check if package.json already exists
1023
+ if (fs.existsSync(pkgPath)) {
1024
+ console.log(chalk_1.default.gray(' package.json already exists, skipping initialization'));
1025
+ return;
1026
+ }
1027
+ const slugName = path.basename(projectDir);
1028
+ // Create package.json
1029
+ const packageJson = {
1030
+ name: slugName,
1031
+ version: '0.1.0',
1032
+ private: true,
1033
+ type: 'module',
1034
+ scripts: {
1035
+ dev: 'vite',
1036
+ build: 'tsc && vite build',
1037
+ preview: 'vite preview',
1038
+ deploy: `thinksoft deploy --app ${appId}`
1039
+ },
1040
+ dependencies: {
1041
+ '@thinksoftai/sdk': '^1.0.0',
1042
+ 'react': '^18.2.0',
1043
+ 'react-dom': '^18.2.0',
1044
+ 'react-router-dom': '^6.20.0',
1045
+ 'lucide-react': '^0.294.0'
1046
+ },
1047
+ devDependencies: {
1048
+ '@types/react': '^18.2.0',
1049
+ '@types/react-dom': '^18.2.0',
1050
+ '@vitejs/plugin-react': '^4.2.0',
1051
+ 'autoprefixer': '^10.4.16',
1052
+ 'postcss': '^8.4.32',
1053
+ 'tailwindcss': '^3.3.6',
1054
+ 'typescript': '^5.3.0',
1055
+ 'vite': '^5.0.0'
1056
+ }
1057
+ };
1058
+ fs.writeFileSync(pkgPath, JSON.stringify(packageJson, null, 2));
1059
+ console.log(chalk_1.default.green(' ✨ Created: package.json'));
1060
+ // Create thinksoft.json
1061
+ const thinksoftJson = {
1062
+ appId,
1063
+ name: appName,
1064
+ framework,
1065
+ outputDir: 'dist'
1066
+ };
1067
+ fs.writeFileSync(path.join(projectDir, 'thinksoft.json'), JSON.stringify(thinksoftJson, null, 2));
1068
+ console.log(chalk_1.default.green(' ✨ Created: thinksoft.json'));
1069
+ // Create vite.config.ts
1070
+ const viteConfig = `import { defineConfig } from 'vite'
1071
+ import react from '@vitejs/plugin-react'
1072
+ import path from 'path'
1073
+
1074
+ export default defineConfig({
1075
+ plugins: [react()],
1076
+ resolve: {
1077
+ alias: {
1078
+ '@': path.resolve(__dirname, './src'),
1079
+ },
1080
+ },
1081
+ })
1082
+ `;
1083
+ fs.writeFileSync(path.join(projectDir, 'vite.config.ts'), viteConfig);
1084
+ console.log(chalk_1.default.green(' ✨ Created: vite.config.ts'));
1085
+ // Create tsconfig.json
1086
+ const tsConfig = {
1087
+ compilerOptions: {
1088
+ target: 'ES2020',
1089
+ useDefineForClassFields: true,
1090
+ lib: ['ES2020', 'DOM', 'DOM.Iterable'],
1091
+ module: 'ESNext',
1092
+ skipLibCheck: true,
1093
+ moduleResolution: 'bundler',
1094
+ allowImportingTsExtensions: true,
1095
+ resolveJsonModule: true,
1096
+ isolatedModules: true,
1097
+ noEmit: true,
1098
+ jsx: 'react-jsx',
1099
+ strict: true,
1100
+ noUnusedLocals: true,
1101
+ noUnusedParameters: true,
1102
+ noFallthroughCasesInSwitch: true,
1103
+ baseUrl: '.',
1104
+ paths: {
1105
+ '@/*': ['./src/*']
1106
+ }
1107
+ },
1108
+ include: ['src'],
1109
+ references: [{ path: './tsconfig.node.json' }]
1110
+ };
1111
+ fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
1112
+ console.log(chalk_1.default.green(' ✨ Created: tsconfig.json'));
1113
+ // Create tsconfig.node.json
1114
+ const tsConfigNode = {
1115
+ compilerOptions: {
1116
+ composite: true,
1117
+ skipLibCheck: true,
1118
+ module: 'ESNext',
1119
+ moduleResolution: 'bundler',
1120
+ allowSyntheticDefaultImports: true
1121
+ },
1122
+ include: ['vite.config.ts']
1123
+ };
1124
+ fs.writeFileSync(path.join(projectDir, 'tsconfig.node.json'), JSON.stringify(tsConfigNode, null, 2));
1125
+ // Create tailwind.config.js
1126
+ const tailwindConfig = `/** @type {import('tailwindcss').Config} */
1127
+ export default {
1128
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
1129
+ theme: {
1130
+ extend: {},
1131
+ },
1132
+ plugins: [],
1133
+ }
1134
+ `;
1135
+ fs.writeFileSync(path.join(projectDir, 'tailwind.config.js'), tailwindConfig);
1136
+ console.log(chalk_1.default.green(' ✨ Created: tailwind.config.js'));
1137
+ // Create postcss.config.js
1138
+ const postcssConfig = `export default {
1139
+ plugins: {
1140
+ tailwindcss: {},
1141
+ autoprefixer: {},
1142
+ },
1143
+ }
1144
+ `;
1145
+ fs.writeFileSync(path.join(projectDir, 'postcss.config.js'), postcssConfig);
1146
+ // Create index.html
1147
+ const indexHtml = `<!DOCTYPE html>
1148
+ <html lang="en">
1149
+ <head>
1150
+ <meta charset="UTF-8" />
1151
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1152
+ <title>${appName}</title>
1153
+ </head>
1154
+ <body>
1155
+ <div id="root"></div>
1156
+ <script type="module" src="/src/main.tsx"></script>
1157
+ </body>
1158
+ </html>
1159
+ `;
1160
+ fs.writeFileSync(path.join(projectDir, 'index.html'), indexHtml);
1161
+ console.log(chalk_1.default.green(' ✨ Created: index.html'));
1162
+ // Create src/main.tsx
1163
+ const srcDir = path.join(projectDir, 'src');
1164
+ if (!fs.existsSync(srcDir)) {
1165
+ fs.mkdirSync(srcDir, { recursive: true });
1166
+ }
1167
+ const mainTsx = `import React from 'react'
1168
+ import ReactDOM from 'react-dom/client'
1169
+ import { BrowserRouter } from 'react-router-dom'
1170
+ import App from './App'
1171
+ import './index.css'
1172
+
1173
+ ReactDOM.createRoot(document.getElementById('root')!).render(
1174
+ <React.StrictMode>
1175
+ <BrowserRouter>
1176
+ <App />
1177
+ </BrowserRouter>
1178
+ </React.StrictMode>,
1179
+ )
1180
+ `;
1181
+ fs.writeFileSync(path.join(srcDir, 'main.tsx'), mainTsx);
1182
+ console.log(chalk_1.default.green(' ✨ Created: src/main.tsx'));
1183
+ // Create src/index.css with Tailwind
1184
+ const indexCss = `@tailwind base;
1185
+ @tailwind components;
1186
+ @tailwind utilities;
1187
+ `;
1188
+ fs.writeFileSync(path.join(srcDir, 'index.css'), indexCss);
1189
+ console.log(chalk_1.default.green(' ✨ Created: src/index.css'));
1190
+ console.log();
1191
+ }
1192
+ /**
1193
+ * Install npm dependencies
1194
+ */
1195
+ async function installDependencies(dependencies) {
1196
+ if (dependencies.length === 0)
1197
+ return;
1198
+ // Check which packages are already installed
1199
+ const pkgPath = path.join(process.cwd(), 'package.json');
1200
+ let installedPackages = new Set();
1201
+ try {
1202
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
1203
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1204
+ installedPackages = new Set(Object.keys(allDeps));
1205
+ }
1206
+ catch {
1207
+ // If can't read package.json, try to install all
1208
+ }
1209
+ // Filter out already installed packages
1210
+ const toInstall = dependencies.filter(dep => {
1211
+ // Extract package name (without version)
1212
+ const pkgName = dep.startsWith('@')
1213
+ ? dep.split('/').slice(0, 2).join('/').split('@').slice(0, 2).join('@')
1214
+ : dep.split('@')[0];
1215
+ return !installedPackages.has(pkgName);
1216
+ });
1217
+ if (toInstall.length === 0) {
1218
+ console.log(chalk_1.default.gray(' All dependencies already installed'));
1219
+ return;
1220
+ }
1221
+ console.log(chalk_1.default.cyan('📦 Installing dependencies:'));
1222
+ for (const dep of toInstall) {
1223
+ console.log(chalk_1.default.gray(` • ${dep}`));
1224
+ }
1225
+ console.log();
1226
+ const spinner = (0, ora_1.default)('Installing packages...').start();
1227
+ try {
1228
+ const packagesStr = toInstall.join(' ');
1229
+ (0, child_process_1.execSync)(`npm install ${packagesStr}`, {
1230
+ stdio: 'pipe',
1231
+ cwd: process.cwd()
1232
+ });
1233
+ spinner.succeed(`Installed ${toInstall.length} package(s)`);
1234
+ }
1235
+ catch (err) {
1236
+ spinner.fail('Failed to install some packages');
1237
+ console.log(chalk_1.default.yellow(' You may need to install manually:'));
1238
+ console.log(chalk_1.default.yellow(` npm install ${toInstall.join(' ')}`));
1239
+ }
1240
+ }
1241
+ //# sourceMappingURL=dev.js.map