@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.
- package/dist/commands/dev.d.ts +9 -0
- package/dist/commands/dev.js +1241 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +28 -0
- package/dist/commands/login.js.map +1 -1
- package/dist/index.js +21 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/thinksoft.d.ts +3 -0
- package/dist/lib/thinksoft.js +10 -0
- package/dist/lib/thinksoft.js.map +1 -0
- package/package.json +10 -9
|
@@ -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
|