@thinksoftai/cli 1.6.11 → 1.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1228 @@
1
+ "use strict";
2
+ /**
3
+ * Export command - export app as self-hosted backend code
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.exportBackend = exportBackend;
43
+ const inquirer_1 = __importDefault(require("inquirer"));
44
+ const ora_1 = __importDefault(require("ora"));
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const chalk_1 = __importDefault(require("chalk"));
48
+ const api = __importStar(require("../utils/api"));
49
+ const config = __importStar(require("../utils/config"));
50
+ const logger = __importStar(require("../utils/logger"));
51
+ async function exportBackend(options = {}) {
52
+ if (!config.isLoggedIn()) {
53
+ logger.error('Not logged in');
54
+ logger.info('Use "thinksoft login" to authenticate');
55
+ return;
56
+ }
57
+ const appId = options.app || config.getProjectConfig()?.appId;
58
+ if (!appId) {
59
+ logger.error('App ID is required');
60
+ logger.info('Use "thinksoft export --app <appId>" or run from a project with thinksoft.json');
61
+ return;
62
+ }
63
+ logger.header('Export Backend Code');
64
+ logger.keyValue('App ID', appId);
65
+ logger.newLine();
66
+ // Fetch app data
67
+ const spinner = (0, ora_1.default)('Fetching app data...').start();
68
+ const exportData = await api.getExportData(appId);
69
+ if (exportData.error) {
70
+ spinner.fail('Failed to fetch app data');
71
+ logger.error(exportData.error);
72
+ return;
73
+ }
74
+ spinner.succeed(`App data fetched: ${exportData.tables?.length || 0} tables, ${exportData.agents?.length || 0} agents`);
75
+ // Interactive prompts
76
+ const answers = await inquirer_1.default.prompt([
77
+ {
78
+ type: 'list',
79
+ name: 'format',
80
+ message: 'Export format:',
81
+ choices: [
82
+ { name: 'Node.js + Express + PostgreSQL', value: 'node-express' },
83
+ { name: 'PostgreSQL Only (schema + triggers)', value: 'postgres-only' }
84
+ ],
85
+ default: options.format || 'node-express'
86
+ },
87
+ {
88
+ type: 'confirm',
89
+ name: 'includeRules',
90
+ message: 'Include business rules as code?',
91
+ default: options.includeRules !== false
92
+ },
93
+ {
94
+ type: 'input',
95
+ name: 'baseUrl',
96
+ message: 'Base URL for API:',
97
+ default: '/api/v1'
98
+ },
99
+ {
100
+ type: 'confirm',
101
+ name: 'customizeEndpoints',
102
+ message: 'Customize endpoint paths?',
103
+ default: false
104
+ }
105
+ ]);
106
+ // Customize endpoints if requested
107
+ const endpointConfig = {};
108
+ if (answers.customizeEndpoints && exportData.tables) {
109
+ for (const table of exportData.tables) {
110
+ const { customPath } = await inquirer_1.default.prompt([
111
+ {
112
+ type: 'input',
113
+ name: 'customPath',
114
+ message: `Endpoint for ${table.name}:`,
115
+ default: `/${table.slug}`
116
+ }
117
+ ]);
118
+ endpointConfig[table.slug] = { path: customPath };
119
+ }
120
+ }
121
+ else if (exportData.tables) {
122
+ // Set default endpoints
123
+ for (const table of exportData.tables) {
124
+ endpointConfig[table.slug] = { path: `/${table.slug}` };
125
+ }
126
+ }
127
+ // Chat option (only for node-express)
128
+ let chatOption = 'none';
129
+ if (answers.format === 'node-express') {
130
+ const chatAnswer = await inquirer_1.default.prompt([
131
+ {
132
+ type: 'list',
133
+ name: 'chatOption',
134
+ message: 'Include AI chat capability?',
135
+ choices: [
136
+ { name: 'No - Action chips only (no setup needed)', value: 'none' },
137
+ { name: 'Yes - OpenAI (requires API key)', value: 'openai' },
138
+ { name: 'Yes - Simple keyword matching (no AI)', value: 'simple' }
139
+ ],
140
+ default: 'none'
141
+ }
142
+ ]);
143
+ chatOption = chatAnswer.chatOption;
144
+ }
145
+ // Output directory
146
+ const appSlug = exportData.app?.name?.toLowerCase().replace(/\s+/g, '-') || appId.toLowerCase();
147
+ const { outputDir } = await inquirer_1.default.prompt([
148
+ {
149
+ type: 'input',
150
+ name: 'outputDir',
151
+ message: 'Output directory:',
152
+ default: options.output || `./${appSlug}-backend`
153
+ }
154
+ ]);
155
+ // Generate files
156
+ const generatorSpinner = (0, ora_1.default)('Generating files...').start();
157
+ try {
158
+ const fileCount = await generateExport(exportData, {
159
+ ...answers,
160
+ chatOption,
161
+ baseUrl: answers.baseUrl
162
+ }, endpointConfig, outputDir);
163
+ generatorSpinner.succeed(`Generated ${fileCount} files`);
164
+ // Success message
165
+ logger.newLine();
166
+ logger.success('Export complete!');
167
+ logger.newLine();
168
+ logger.info('Next steps:');
169
+ logger.log(` cd ${outputDir}`);
170
+ logger.log(' npm install');
171
+ logger.log(' cp .env.example .env');
172
+ logger.log(' # Edit .env with your database credentials');
173
+ if (answers.format === 'node-express') {
174
+ logger.log(' npm run db:setup');
175
+ logger.log(' npm start');
176
+ logger.newLine();
177
+ logger.log(chalk_1.default.gray(` API will be available at http://localhost:3000${answers.baseUrl}`));
178
+ }
179
+ else {
180
+ logger.log(' # Run schema.sql in your PostgreSQL database');
181
+ }
182
+ }
183
+ catch (error) {
184
+ generatorSpinner.fail('Export failed');
185
+ logger.error(error.message || error);
186
+ }
187
+ }
188
+ async function generateExport(data, config, endpoints, outputDir) {
189
+ let fileCount = 0;
190
+ // Create base directory
191
+ fs.mkdirSync(outputDir, { recursive: true });
192
+ if (config.format === 'postgres-only') {
193
+ // PostgreSQL only - just schema and triggers
194
+ fs.mkdirSync(path.join(outputDir, 'database'), { recursive: true });
195
+ // Generate schema
196
+ const schema = generateSchema(data.tables);
197
+ fs.writeFileSync(path.join(outputDir, 'database/schema.sql'), schema);
198
+ fileCount++;
199
+ // Generate triggers if rules included
200
+ if (config.includeRules && data.rules?.length > 0) {
201
+ const triggers = generateTriggers(data.rules);
202
+ fs.writeFileSync(path.join(outputDir, 'database/triggers.sql'), triggers);
203
+ fileCount++;
204
+ }
205
+ // Generate README
206
+ const readme = generatePostgresReadme(data.app);
207
+ fs.writeFileSync(path.join(outputDir, 'README.md'), readme);
208
+ fileCount++;
209
+ }
210
+ else {
211
+ // Full Node.js + Express export
212
+ const dirs = [
213
+ 'database',
214
+ 'database/migrations',
215
+ 'src',
216
+ 'src/controllers',
217
+ 'src/routes',
218
+ 'src/middleware',
219
+ 'src/validators',
220
+ 'src/hooks',
221
+ 'src/events',
222
+ 'agents',
223
+ 'client'
224
+ ];
225
+ for (const dir of dirs) {
226
+ fs.mkdirSync(path.join(outputDir, dir), { recursive: true });
227
+ }
228
+ // Database files
229
+ const schema = generateSchema(data.tables);
230
+ fs.writeFileSync(path.join(outputDir, 'database/schema.sql'), schema);
231
+ fileCount++;
232
+ if (config.includeRules && data.rules?.length > 0) {
233
+ const triggers = generateTriggers(data.rules);
234
+ fs.writeFileSync(path.join(outputDir, 'database/triggers.sql'), triggers);
235
+ fileCount++;
236
+ }
237
+ // Controllers
238
+ for (const table of data.tables || []) {
239
+ const controller = generateController(table, config.includeRules);
240
+ fs.writeFileSync(path.join(outputDir, `src/controllers/${table.slug}.js`), controller);
241
+ fileCount++;
242
+ }
243
+ // Validators
244
+ for (const table of data.tables || []) {
245
+ const validator = generateValidator(table);
246
+ fs.writeFileSync(path.join(outputDir, `src/validators/${table.slug}.js`), validator);
247
+ fileCount++;
248
+ }
249
+ // Hooks (if rules included)
250
+ if (config.includeRules) {
251
+ for (const table of data.tables || []) {
252
+ const tableRules = (data.rules || []).filter((r) => r.table_slug === table.slug);
253
+ const hooks = generateHooks(table, tableRules);
254
+ fs.writeFileSync(path.join(outputDir, `src/hooks/${table.slug}.js`), hooks);
255
+ fileCount++;
256
+ }
257
+ }
258
+ // Routes
259
+ const routes = generateRoutes(data.tables, endpoints, config.baseUrl);
260
+ fs.writeFileSync(path.join(outputDir, 'src/routes/index.js'), routes);
261
+ fileCount++;
262
+ // Agent routes
263
+ const agentRoutes = generateAgentRoutes();
264
+ fs.writeFileSync(path.join(outputDir, 'src/routes/agents.js'), agentRoutes);
265
+ fileCount++;
266
+ // Agents
267
+ const agentConfig = generateAgentConfig(data.agents);
268
+ fs.writeFileSync(path.join(outputDir, 'agents/config.json'), agentConfig);
269
+ fileCount++;
270
+ const agentActions = generateAgentActions();
271
+ fs.writeFileSync(path.join(outputDir, 'agents/actions.js'), agentActions);
272
+ fileCount++;
273
+ // Agent middleware
274
+ const agentMiddleware = generateAgentMiddleware();
275
+ fs.writeFileSync(path.join(outputDir, 'src/middleware/agentAuth.js'), agentMiddleware);
276
+ fileCount++;
277
+ // Chat (if enabled)
278
+ if (config.chatOption !== 'none') {
279
+ const chat = generateChat(config.chatOption);
280
+ fs.writeFileSync(path.join(outputDir, 'agents/chat.js'), chat);
281
+ fileCount++;
282
+ }
283
+ // Frontend client
284
+ const client = generateClient(data.tables, endpoints, config.baseUrl);
285
+ fs.writeFileSync(path.join(outputDir, 'client/api.js'), client);
286
+ fileCount++;
287
+ // Events
288
+ const events = generateEvents(data.tables);
289
+ fs.writeFileSync(path.join(outputDir, 'src/events/index.js'), events);
290
+ fileCount++;
291
+ // Main files
292
+ const indexJs = generateIndex(config.baseUrl);
293
+ fs.writeFileSync(path.join(outputDir, 'src/index.js'), indexJs);
294
+ fileCount++;
295
+ const dbJs = generateDb();
296
+ fs.writeFileSync(path.join(outputDir, 'src/db.js'), dbJs);
297
+ fileCount++;
298
+ const authMiddleware = generateAuthMiddleware();
299
+ fs.writeFileSync(path.join(outputDir, 'src/middleware/auth.js'), authMiddleware);
300
+ fileCount++;
301
+ // Config files
302
+ const apiConfig = generateApiConfig(data.tables, endpoints, config);
303
+ fs.writeFileSync(path.join(outputDir, 'api.config.json'), apiConfig);
304
+ fileCount++;
305
+ const packageJson = generatePackageJson(data.app, config);
306
+ fs.writeFileSync(path.join(outputDir, 'package.json'), packageJson);
307
+ fileCount++;
308
+ // Docker
309
+ const dockerCompose = generateDockerCompose(data.app);
310
+ fs.writeFileSync(path.join(outputDir, 'docker-compose.yml'), dockerCompose);
311
+ fileCount++;
312
+ const dockerfile = generateDockerfile();
313
+ fs.writeFileSync(path.join(outputDir, 'Dockerfile'), dockerfile);
314
+ fileCount++;
315
+ // Env and readme
316
+ const envExample = generateEnvExample(config);
317
+ fs.writeFileSync(path.join(outputDir, '.env.example'), envExample);
318
+ fileCount++;
319
+ const readme = generateReadme(data.app, config);
320
+ fs.writeFileSync(path.join(outputDir, 'README.md'), readme);
321
+ fileCount++;
322
+ }
323
+ return fileCount;
324
+ }
325
+ // ============================================
326
+ // Generator Functions
327
+ // ============================================
328
+ function generateSchema(tables) {
329
+ let sql = '-- Auto-generated PostgreSQL schema\n';
330
+ sql += '-- Generated by ThinkSoft CLI\n\n';
331
+ sql += 'CREATE EXTENSION IF NOT EXISTS "pgcrypto";\n\n';
332
+ const typeMap = {
333
+ 'text': 'VARCHAR(255)',
334
+ 'textarea': 'TEXT',
335
+ 'number': 'DECIMAL(10,2)',
336
+ 'integer': 'INTEGER',
337
+ 'email': 'VARCHAR(255)',
338
+ 'phone': 'VARCHAR(50)',
339
+ 'url': 'VARCHAR(500)',
340
+ 'date': 'DATE',
341
+ 'datetime': 'TIMESTAMP',
342
+ 'time': 'TIME',
343
+ 'boolean': 'BOOLEAN DEFAULT FALSE',
344
+ 'file': 'VARCHAR(500)',
345
+ 'image': 'VARCHAR(500)',
346
+ 'location': 'JSONB',
347
+ 'json': 'JSONB',
348
+ 'multiselect': 'TEXT[]',
349
+ 'select': 'VARCHAR(100)',
350
+ 'radio': 'VARCHAR(100)'
351
+ };
352
+ for (const table of tables || []) {
353
+ sql += `-- Table: ${table.name}\n`;
354
+ sql += `CREATE TABLE ${table.slug} (\n`;
355
+ sql += ` id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n`;
356
+ for (const field of table.fields || []) {
357
+ let fieldType = typeMap[field.type] || 'TEXT';
358
+ // Handle select/radio with CHECK constraint
359
+ if ((field.type === 'select' || field.type === 'radio') && field.options?.length) {
360
+ const options = field.options.map((o) => `'${o}'`).join(', ');
361
+ fieldType = `VARCHAR(100) CHECK (${field.slug} IN (${options}))`;
362
+ }
363
+ // Handle reference
364
+ if (field.type === 'reference' && field.reference_table) {
365
+ fieldType = `UUID REFERENCES ${field.reference_table}(id)`;
366
+ }
367
+ // Add NOT NULL for required
368
+ if (field.required && !fieldType.includes('DEFAULT')) {
369
+ fieldType += ' NOT NULL';
370
+ }
371
+ sql += ` ${field.slug} ${fieldType},\n`;
372
+ }
373
+ sql += ` created_at TIMESTAMP DEFAULT NOW(),\n`;
374
+ sql += ` updated_at TIMESTAMP DEFAULT NOW()\n`;
375
+ sql += `);\n\n`;
376
+ }
377
+ // Add updated_at trigger
378
+ sql += `-- Auto-update updated_at timestamp\n`;
379
+ sql += `CREATE OR REPLACE FUNCTION update_timestamp()\n`;
380
+ sql += `RETURNS TRIGGER AS $$\n`;
381
+ sql += `BEGIN\n`;
382
+ sql += ` NEW.updated_at = NOW();\n`;
383
+ sql += ` RETURN NEW;\n`;
384
+ sql += `END;\n`;
385
+ sql += `$$ LANGUAGE plpgsql;\n\n`;
386
+ for (const table of tables || []) {
387
+ sql += `CREATE TRIGGER ${table.slug}_updated_at\n`;
388
+ sql += `BEFORE UPDATE ON ${table.slug}\n`;
389
+ sql += `FOR EACH ROW EXECUTE FUNCTION update_timestamp();\n\n`;
390
+ }
391
+ return sql;
392
+ }
393
+ function generateTriggers(rules) {
394
+ let sql = '-- Auto-generated database triggers from business rules\n';
395
+ sql += '-- Generated by ThinkSoft CLI\n\n';
396
+ // Group by table
397
+ const rulesByTable = {};
398
+ for (const rule of rules || []) {
399
+ if (!rulesByTable[rule.table_slug]) {
400
+ rulesByTable[rule.table_slug] = [];
401
+ }
402
+ rulesByTable[rule.table_slug].push(rule);
403
+ }
404
+ for (const [table, tableRules] of Object.entries(rulesByTable)) {
405
+ sql += `-- Rules for ${table}\n`;
406
+ sql += `CREATE OR REPLACE FUNCTION ${table}_rules()\n`;
407
+ sql += `RETURNS TRIGGER AS $$\n`;
408
+ sql += `BEGIN\n`;
409
+ for (const rule of tableRules) {
410
+ sql += ` -- ${rule.name}\n`;
411
+ // Add rule logic here based on conditions/actions
412
+ }
413
+ sql += ` RETURN NEW;\n`;
414
+ sql += `END;\n`;
415
+ sql += `$$ LANGUAGE plpgsql;\n\n`;
416
+ }
417
+ return sql;
418
+ }
419
+ function generateController(table, includeHooks) {
420
+ return `/**
421
+ * ${table.name} Controller
422
+ * Auto-generated by ThinkSoft CLI
423
+ */
424
+
425
+ const db = require('../db');
426
+ ${includeHooks ? `const hooks = require('../hooks/${table.slug}');` : ''}
427
+ const eventBus = require('../events');
428
+
429
+ module.exports = {
430
+ list: async (req, res) => {
431
+ try {
432
+ const { limit = 50, offset = 0 } = req.query;
433
+ const data = await db.query(
434
+ 'SELECT * FROM ${table.slug} ORDER BY created_at DESC LIMIT $1 OFFSET $2',
435
+ [limit, offset]
436
+ );
437
+ res.json({ data: data.rows });
438
+ } catch (error) {
439
+ res.status(500).json({ error: error.message });
440
+ }
441
+ },
442
+
443
+ get: async (req, res) => {
444
+ try {
445
+ const { id } = req.params;
446
+ const result = await db.query('SELECT * FROM ${table.slug} WHERE id = $1', [id]);
447
+ if (result.rows.length === 0) {
448
+ return res.status(404).json({ error: 'Not found' });
449
+ }
450
+ res.json({ data: result.rows[0] });
451
+ } catch (error) {
452
+ res.status(500).json({ error: error.message });
453
+ }
454
+ },
455
+
456
+ create: async (req, res) => {
457
+ try {
458
+ let data = req.body;
459
+ ${includeHooks ? `if (hooks.beforeCreate) data = await hooks.beforeCreate(data, db);` : ''}
460
+
461
+ const fields = Object.keys(data);
462
+ const values = Object.values(data);
463
+ const placeholders = fields.map((_, i) => '$' + (i + 1));
464
+
465
+ const result = await db.query(
466
+ \`INSERT INTO ${table.slug} (\${fields.join(', ')}) VALUES (\${placeholders.join(', ')}) RETURNING *\`,
467
+ values
468
+ );
469
+
470
+ const record = result.rows[0];
471
+ ${includeHooks ? `if (hooks.afterCreate) await hooks.afterCreate(record, db);` : ''}
472
+ eventBus.emit('${table.slug}.created', { new: record });
473
+
474
+ res.status(201).json({ data: record });
475
+ } catch (error) {
476
+ res.status(500).json({ error: error.message });
477
+ }
478
+ },
479
+
480
+ update: async (req, res) => {
481
+ try {
482
+ const { id } = req.params;
483
+ let data = req.body;
484
+
485
+ const oldResult = await db.query('SELECT * FROM ${table.slug} WHERE id = $1', [id]);
486
+ if (oldResult.rows.length === 0) {
487
+ return res.status(404).json({ error: 'Not found' });
488
+ }
489
+ const oldRecord = oldResult.rows[0];
490
+
491
+ ${includeHooks ? `if (hooks.beforeUpdate) data = await hooks.beforeUpdate(oldRecord, data, db);` : ''}
492
+
493
+ const fields = Object.keys(data);
494
+ const values = Object.values(data);
495
+ const setClause = fields.map((f, i) => \`\${f} = $\${i + 1}\`).join(', ');
496
+
497
+ const result = await db.query(
498
+ \`UPDATE ${table.slug} SET \${setClause} WHERE id = $\${fields.length + 1} RETURNING *\`,
499
+ [...values, id]
500
+ );
501
+
502
+ const record = result.rows[0];
503
+ ${includeHooks ? `if (hooks.afterUpdate) await hooks.afterUpdate(oldRecord, record, db);` : ''}
504
+ eventBus.emit('${table.slug}.updated', { old: oldRecord, new: record });
505
+
506
+ res.json({ data: record });
507
+ } catch (error) {
508
+ res.status(500).json({ error: error.message });
509
+ }
510
+ },
511
+
512
+ delete: async (req, res) => {
513
+ try {
514
+ const { id } = req.params;
515
+
516
+ const oldResult = await db.query('SELECT * FROM ${table.slug} WHERE id = $1', [id]);
517
+ if (oldResult.rows.length === 0) {
518
+ return res.status(404).json({ error: 'Not found' });
519
+ }
520
+ const oldRecord = oldResult.rows[0];
521
+
522
+ ${includeHooks ? `if (hooks.beforeDelete) await hooks.beforeDelete(oldRecord, db);` : ''}
523
+
524
+ await db.query('DELETE FROM ${table.slug} WHERE id = $1', [id]);
525
+
526
+ ${includeHooks ? `if (hooks.afterDelete) await hooks.afterDelete(oldRecord, db);` : ''}
527
+ eventBus.emit('${table.slug}.deleted', { old: oldRecord });
528
+
529
+ res.json({ success: true });
530
+ } catch (error) {
531
+ res.status(500).json({ error: error.message });
532
+ }
533
+ }
534
+ };
535
+ `;
536
+ }
537
+ function generateValidator(table) {
538
+ let schemaFields = '';
539
+ for (const field of table.fields || []) {
540
+ let validator = 'Joi.string()';
541
+ switch (field.type) {
542
+ case 'number':
543
+ case 'integer':
544
+ validator = 'Joi.number()';
545
+ break;
546
+ case 'boolean':
547
+ validator = 'Joi.boolean()';
548
+ break;
549
+ case 'email':
550
+ validator = 'Joi.string().email()';
551
+ break;
552
+ case 'date':
553
+ case 'datetime':
554
+ validator = 'Joi.date()';
555
+ break;
556
+ case 'select':
557
+ case 'radio':
558
+ if (field.options?.length) {
559
+ validator = `Joi.string().valid(${field.options.map((o) => `'${o}'`).join(', ')})`;
560
+ }
561
+ break;
562
+ }
563
+ if (field.required) {
564
+ validator += '.required()';
565
+ }
566
+ else {
567
+ validator += '.optional().allow(null, \'\')';
568
+ }
569
+ schemaFields += ` ${field.slug}: ${validator},\n`;
570
+ }
571
+ return `/**
572
+ * ${table.name} Validator
573
+ * Auto-generated by ThinkSoft CLI
574
+ */
575
+
576
+ const Joi = require('joi');
577
+
578
+ const schema = Joi.object({
579
+ ${schemaFields}});
580
+
581
+ const validate = (req, res, next) => {
582
+ const { error, value } = schema.validate(req.body, { stripUnknown: true });
583
+
584
+ if (error) {
585
+ return res.status(400).json({
586
+ error: 'Validation failed',
587
+ details: error.details.map(d => d.message)
588
+ });
589
+ }
590
+
591
+ req.body = value;
592
+ next();
593
+ };
594
+
595
+ module.exports = { schema, validate };
596
+ `;
597
+ }
598
+ function generateHooks(table, rules) {
599
+ return `/**
600
+ * ${table.name} Hooks
601
+ * Auto-generated by ThinkSoft CLI
602
+ */
603
+
604
+ module.exports = {
605
+ beforeCreate: async (data, db) => {
606
+ // Add custom logic here
607
+ return data;
608
+ },
609
+
610
+ afterCreate: async (record, db) => {
611
+ // Add custom logic here
612
+ },
613
+
614
+ beforeUpdate: async (oldRecord, data, db) => {
615
+ // Add custom logic here
616
+ return data;
617
+ },
618
+
619
+ afterUpdate: async (oldRecord, newRecord, db) => {
620
+ // Add custom logic here
621
+ },
622
+
623
+ beforeDelete: async (record, db) => {
624
+ // Add custom logic here
625
+ },
626
+
627
+ afterDelete: async (record, db) => {
628
+ // Add custom logic here
629
+ }
630
+ };
631
+ `;
632
+ }
633
+ function generateRoutes(tables, endpoints, baseUrl) {
634
+ let routes = `/**
635
+ * API Routes
636
+ * Auto-generated by ThinkSoft CLI
637
+ */
638
+
639
+ const express = require('express');
640
+ const router = express.Router();
641
+ const { authenticate } = require('../middleware/auth');
642
+
643
+ `;
644
+ for (const table of tables || []) {
645
+ const endpoint = endpoints[table.slug]?.path || `/${table.slug}`;
646
+ routes += `// ${table.name}\n`;
647
+ routes += `const ${table.slug}Controller = require('../controllers/${table.slug}');\n`;
648
+ routes += `const ${table.slug}Validator = require('../validators/${table.slug}');\n\n`;
649
+ routes += `router.get('${endpoint}', authenticate, ${table.slug}Controller.list);\n`;
650
+ routes += `router.get('${endpoint}/:id', authenticate, ${table.slug}Controller.get);\n`;
651
+ routes += `router.post('${endpoint}', authenticate, ${table.slug}Validator.validate, ${table.slug}Controller.create);\n`;
652
+ routes += `router.put('${endpoint}/:id', authenticate, ${table.slug}Validator.validate, ${table.slug}Controller.update);\n`;
653
+ routes += `router.delete('${endpoint}/:id', authenticate, ${table.slug}Controller.delete);\n\n`;
654
+ }
655
+ routes += `module.exports = router;\n`;
656
+ return routes;
657
+ }
658
+ function generateAgentRoutes() {
659
+ return `/**
660
+ * Agent Routes
661
+ * Auto-generated by ThinkSoft CLI
662
+ */
663
+
664
+ const express = require('express');
665
+ const router = express.Router();
666
+ const { authenticate } = require('../middleware/auth');
667
+ const agentAuth = require('../middleware/agentAuth');
668
+ const { getActionChips, executeAction } = require('../../agents/actions');
669
+ const agentConfig = require('../../agents/config.json');
670
+
671
+ // Get agent info
672
+ router.get('/:agent', authenticate, (req, res) => {
673
+ const agent = agentConfig[req.params.agent];
674
+ if (!agent) {
675
+ return res.status(404).json({ error: 'Agent not found' });
676
+ }
677
+ res.json({
678
+ name: agent.name,
679
+ description: agent.description,
680
+ welcome_message: agent.welcome_message,
681
+ action_chips: agent.action_chips
682
+ });
683
+ });
684
+
685
+ // Execute action chip
686
+ router.post('/:agent/action', authenticate, async (req, res) => {
687
+ try {
688
+ const { action } = req.body;
689
+ const result = await executeAction(req.params.agent, action, req.user?.id);
690
+ res.json(result);
691
+ } catch (error) {
692
+ res.status(400).json({ error: error.message });
693
+ }
694
+ });
695
+
696
+ // Agent-scoped CRUD
697
+ router.get('/:agent/:table', authenticate, agentAuth, async (req, res) => {
698
+ const controller = require(\`../controllers/\${req.params.table}\`);
699
+ return controller.list(req, res);
700
+ });
701
+
702
+ router.get('/:agent/:table/:id', authenticate, agentAuth, async (req, res) => {
703
+ const controller = require(\`../controllers/\${req.params.table}\`);
704
+ return controller.get(req, res);
705
+ });
706
+
707
+ router.post('/:agent/:table', authenticate, agentAuth, async (req, res) => {
708
+ const controller = require(\`../controllers/\${req.params.table}\`);
709
+ return controller.create(req, res);
710
+ });
711
+
712
+ router.put('/:agent/:table/:id', authenticate, agentAuth, async (req, res) => {
713
+ const controller = require(\`../controllers/\${req.params.table}\`);
714
+ return controller.update(req, res);
715
+ });
716
+
717
+ module.exports = router;
718
+ `;
719
+ }
720
+ function generateAgentConfig(agents) {
721
+ const config = {};
722
+ for (const agent of agents || []) {
723
+ config[agent.slug] = {
724
+ name: agent.name,
725
+ slug: agent.slug,
726
+ description: agent.description,
727
+ welcome_message: agent.welcome_message,
728
+ auth_type: agent.auth_type || 'authenticated',
729
+ permissions: agent.permissions || {},
730
+ user_filter: agent.user_filter || {},
731
+ action_chips: (agent.action_chips || []).map((chip) => ({
732
+ label: chip.label,
733
+ icon: chip.icon,
734
+ action: chip.action,
735
+ table: chip.table,
736
+ display: chip.display || 'table',
737
+ filter: chip.filter,
738
+ fields: chip.fields
739
+ }))
740
+ };
741
+ }
742
+ return JSON.stringify(config, null, 2);
743
+ }
744
+ function generateAgentActions() {
745
+ return `/**
746
+ * Agent Actions Handler
747
+ * Auto-generated by ThinkSoft CLI
748
+ */
749
+
750
+ const agentConfig = require('./config.json');
751
+ const db = require('../src/db');
752
+
753
+ const getActionChips = (agentSlug) => {
754
+ const agent = agentConfig[agentSlug];
755
+ return agent?.action_chips || [];
756
+ };
757
+
758
+ const executeAction = async (agentSlug, actionLabel, userId) => {
759
+ const agent = agentConfig[agentSlug];
760
+ if (!agent) throw new Error('Agent not found');
761
+
762
+ const chip = agent.action_chips.find(c => c.label === actionLabel);
763
+ if (!chip) throw new Error('Action not found');
764
+
765
+ let filter = chip.filter;
766
+ if (filter && userId) {
767
+ filter = filter.replace(/:current_user_id/g, userId);
768
+ }
769
+
770
+ switch (chip.action) {
771
+ case 'list':
772
+ const listResult = await db.query(
773
+ \`SELECT * FROM \${chip.table}\${filter ? ' WHERE ' + filter : ''} ORDER BY created_at DESC LIMIT 50\`
774
+ );
775
+ return {
776
+ type: 'list',
777
+ display: chip.display || 'table',
778
+ title: chip.label,
779
+ table: chip.table,
780
+ data: listResult.rows
781
+ };
782
+
783
+ case 'get':
784
+ const getResult = await db.query(
785
+ \`SELECT * FROM \${chip.table} WHERE \${filter || 'TRUE'} LIMIT 1\`
786
+ );
787
+ return {
788
+ type: 'record',
789
+ display: chip.display || 'card',
790
+ title: chip.label,
791
+ table: chip.table,
792
+ data: getResult.rows[0]
793
+ };
794
+
795
+ case 'create':
796
+ return {
797
+ type: 'form',
798
+ display: 'form',
799
+ title: chip.label,
800
+ table: chip.table,
801
+ mode: 'create'
802
+ };
803
+
804
+ default:
805
+ throw new Error('Unknown action: ' + chip.action);
806
+ }
807
+ };
808
+
809
+ module.exports = { getActionChips, executeAction };
810
+ `;
811
+ }
812
+ function generateAgentMiddleware() {
813
+ return `/**
814
+ * Agent Permission Middleware
815
+ * Auto-generated by ThinkSoft CLI
816
+ */
817
+
818
+ const agentConfig = require('../../agents/config.json');
819
+
820
+ const agentAuth = (req, res, next) => {
821
+ const agent = agentConfig[req.params.agent];
822
+ if (!agent) {
823
+ return res.status(404).json({ error: 'Agent not found' });
824
+ }
825
+
826
+ const table = req.params.table;
827
+ const permissions = agent.permissions[table];
828
+
829
+ if (!permissions) {
830
+ return res.status(403).json({ error: 'No access to this table' });
831
+ }
832
+
833
+ const method = req.method;
834
+ const allowed =
835
+ (method === 'GET' && permissions.read) ||
836
+ (method === 'POST' && permissions.create) ||
837
+ (method === 'PUT' && permissions.update) ||
838
+ (method === 'DELETE' && permissions.delete);
839
+
840
+ if (!allowed) {
841
+ return res.status(403).json({ error: 'Permission denied' });
842
+ }
843
+
844
+ if (agent.user_filter?.[table] && req.user) {
845
+ req.userFilter = agent.user_filter[table].replace(/:current_user_id/g, req.user.id);
846
+ }
847
+
848
+ req.agent = agent;
849
+ next();
850
+ };
851
+
852
+ module.exports = agentAuth;
853
+ `;
854
+ }
855
+ function generateChat(option) {
856
+ if (option === 'openai') {
857
+ return `/**
858
+ * Agent Chat (OpenAI)
859
+ * Auto-generated by ThinkSoft CLI
860
+ */
861
+
862
+ const { OpenAI } = require('openai');
863
+ const agentConfig = require('./config.json');
864
+
865
+ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
866
+
867
+ const chat = async (agentSlug, message, history = []) => {
868
+ const agent = agentConfig[agentSlug];
869
+ if (!agent) throw new Error('Agent not found');
870
+
871
+ const systemPrompt = \`You are \${agent.name}. \${agent.description}
872
+ Available actions: \${agent.action_chips.map(c => c.label).join(', ')}\`;
873
+
874
+ const response = await openai.chat.completions.create({
875
+ model: 'gpt-4',
876
+ messages: [
877
+ { role: 'system', content: systemPrompt },
878
+ ...history,
879
+ { role: 'user', content: message }
880
+ ]
881
+ });
882
+
883
+ return { message: response.choices[0].message.content };
884
+ };
885
+
886
+ module.exports = { chat };
887
+ `;
888
+ }
889
+ return `/**
890
+ * Agent Chat (Simple Keyword Matching)
891
+ * Auto-generated by ThinkSoft CLI
892
+ */
893
+
894
+ const agentConfig = require('./config.json');
895
+
896
+ const chat = async (agentSlug, message) => {
897
+ const agent = agentConfig[agentSlug];
898
+ if (!agent) throw new Error('Agent not found');
899
+
900
+ const lowerMessage = message.toLowerCase();
901
+
902
+ for (const chip of agent.action_chips) {
903
+ if (lowerMessage.includes(chip.label.toLowerCase())) {
904
+ return {
905
+ message: \`I can help you with \${chip.label}!\`,
906
+ action: chip.label
907
+ };
908
+ }
909
+ }
910
+
911
+ return {
912
+ message: \`I can help you with: \${agent.action_chips.map(c => c.label).join(', ')}\`,
913
+ action: null
914
+ };
915
+ };
916
+
917
+ module.exports = { chat };
918
+ `;
919
+ }
920
+ function generateClient(tables, endpoints, baseUrl) {
921
+ let code = `/**
922
+ * API Client
923
+ * Auto-generated by ThinkSoft CLI
924
+ */
925
+
926
+ const API_BASE = process.env.REACT_APP_API_URL || 'http://localhost:3000${baseUrl}';
927
+
928
+ const getHeaders = () => ({
929
+ 'Content-Type': 'application/json',
930
+ ...(localStorage.getItem('token') && {
931
+ 'Authorization': \`Bearer \${localStorage.getItem('token')}\`
932
+ })
933
+ });
934
+
935
+ const handleResponse = async (response) => {
936
+ const data = await response.json();
937
+ if (!response.ok) throw new Error(data.error || 'Request failed');
938
+ return data;
939
+ };
940
+
941
+ const api = {
942
+ `;
943
+ for (const table of tables || []) {
944
+ const path = endpoints[table.slug]?.path || `/${table.slug}`;
945
+ code += ` ${table.slug}: {
946
+ list: (params = {}) => fetch(\`\${API_BASE}${path}?\${new URLSearchParams(params)}\`, { headers: getHeaders() }).then(handleResponse),
947
+ get: (id) => fetch(\`\${API_BASE}${path}/\${id}\`, { headers: getHeaders() }).then(handleResponse),
948
+ create: (data) => fetch(\`\${API_BASE}${path}\`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(data) }).then(handleResponse),
949
+ update: (id, data) => fetch(\`\${API_BASE}${path}/\${id}\`, { method: 'PUT', headers: getHeaders(), body: JSON.stringify(data) }).then(handleResponse),
950
+ delete: (id) => fetch(\`\${API_BASE}${path}/\${id}\`, { method: 'DELETE', headers: getHeaders() }).then(handleResponse),
951
+ },
952
+ `;
953
+ }
954
+ code += `};
955
+
956
+ export default api;
957
+ `;
958
+ return code;
959
+ }
960
+ function generateEvents(tables) {
961
+ return `/**
962
+ * Event Bus
963
+ * Auto-generated by ThinkSoft CLI
964
+ */
965
+
966
+ const EventEmitter = require('events');
967
+ const eventBus = new EventEmitter();
968
+
969
+ // Add event listeners here
970
+ // eventBus.on('orders.created', async ({ new: record }) => { ... });
971
+
972
+ module.exports = eventBus;
973
+ `;
974
+ }
975
+ function generateIndex(baseUrl) {
976
+ return `/**
977
+ * Server Entry Point
978
+ * Auto-generated by ThinkSoft CLI
979
+ */
980
+
981
+ require('dotenv').config();
982
+ const express = require('express');
983
+ const cors = require('cors');
984
+ const routes = require('./routes');
985
+ const agentRoutes = require('./routes/agents');
986
+
987
+ const app = express();
988
+ const PORT = process.env.PORT || 3000;
989
+
990
+ app.use(cors());
991
+ app.use(express.json());
992
+
993
+ // Routes
994
+ app.use('${baseUrl}', routes);
995
+ app.use('${baseUrl}/agent', agentRoutes);
996
+
997
+ // Health check
998
+ app.get('/health', (req, res) => res.json({ status: 'ok' }));
999
+
1000
+ app.listen(PORT, () => {
1001
+ console.log(\`Server running on http://localhost:\${PORT}\`);
1002
+ console.log(\`API available at http://localhost:\${PORT}${baseUrl}\`);
1003
+ });
1004
+ `;
1005
+ }
1006
+ function generateDb() {
1007
+ return `/**
1008
+ * Database Connection
1009
+ * Auto-generated by ThinkSoft CLI
1010
+ */
1011
+
1012
+ const { Pool } = require('pg');
1013
+
1014
+ const pool = new Pool({
1015
+ connectionString: process.env.DATABASE_URL,
1016
+ });
1017
+
1018
+ module.exports = {
1019
+ query: (text, params) => pool.query(text, params),
1020
+ pool
1021
+ };
1022
+ `;
1023
+ }
1024
+ function generateAuthMiddleware() {
1025
+ return `/**
1026
+ * Authentication Middleware
1027
+ * Auto-generated by ThinkSoft CLI
1028
+ */
1029
+
1030
+ const jwt = require('jsonwebtoken');
1031
+
1032
+ const authenticate = (req, res, next) => {
1033
+ const authHeader = req.headers.authorization;
1034
+
1035
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
1036
+ return res.status(401).json({ error: 'No token provided' });
1037
+ }
1038
+
1039
+ const token = authHeader.split(' ')[1];
1040
+
1041
+ try {
1042
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
1043
+ req.user = decoded;
1044
+ next();
1045
+ } catch (error) {
1046
+ return res.status(401).json({ error: 'Invalid token' });
1047
+ }
1048
+ };
1049
+
1050
+ module.exports = { authenticate };
1051
+ `;
1052
+ }
1053
+ function generateApiConfig(tables, endpoints, config) {
1054
+ const apiConfig = {
1055
+ baseUrl: config.baseUrl,
1056
+ auth: { type: 'jwt' },
1057
+ endpoints: {}
1058
+ };
1059
+ for (const table of tables || []) {
1060
+ apiConfig.endpoints[table.slug] = {
1061
+ path: endpoints[table.slug]?.path || `/${table.slug}`,
1062
+ methods: ['GET', 'POST', 'PUT', 'DELETE'],
1063
+ auth: true
1064
+ };
1065
+ }
1066
+ return JSON.stringify(apiConfig, null, 2);
1067
+ }
1068
+ function generatePackageJson(app, config) {
1069
+ const pkg = {
1070
+ name: app?.name?.toLowerCase().replace(/\s+/g, '-') + '-backend' || 'thinksoft-backend',
1071
+ version: '1.0.0',
1072
+ description: app?.description || 'Auto-generated backend by ThinkSoft CLI',
1073
+ main: 'src/index.js',
1074
+ scripts: {
1075
+ start: 'node src/index.js',
1076
+ dev: 'nodemon src/index.js',
1077
+ 'db:setup': 'psql $DATABASE_URL -f database/schema.sql'
1078
+ },
1079
+ dependencies: {
1080
+ express: '^4.18.2',
1081
+ cors: '^2.8.5',
1082
+ pg: '^8.11.3',
1083
+ dotenv: '^16.3.1',
1084
+ jsonwebtoken: '^9.0.2',
1085
+ joi: '^17.11.0'
1086
+ },
1087
+ devDependencies: {
1088
+ nodemon: '^3.0.2'
1089
+ }
1090
+ };
1091
+ if (config.chatOption === 'openai') {
1092
+ pkg.dependencies.openai = '^4.20.0';
1093
+ }
1094
+ return JSON.stringify(pkg, null, 2);
1095
+ }
1096
+ function generateDockerCompose(app) {
1097
+ const name = app?.name?.toLowerCase().replace(/\s+/g, '-') || 'app';
1098
+ return `version: '3.8'
1099
+
1100
+ services:
1101
+ db:
1102
+ image: postgres:15
1103
+ environment:
1104
+ POSTGRES_DB: ${name}
1105
+ POSTGRES_USER: postgres
1106
+ POSTGRES_PASSWORD: postgres
1107
+ volumes:
1108
+ - postgres_data:/var/lib/postgresql/data
1109
+ - ./database/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql
1110
+ ports:
1111
+ - "5432:5432"
1112
+
1113
+ api:
1114
+ build: .
1115
+ depends_on:
1116
+ - db
1117
+ environment:
1118
+ DATABASE_URL: postgresql://postgres:postgres@db:5432/${name}
1119
+ JWT_SECRET: your-secret-key-change-in-production
1120
+ PORT: 3000
1121
+ ports:
1122
+ - "3000:3000"
1123
+
1124
+ volumes:
1125
+ postgres_data:
1126
+ `;
1127
+ }
1128
+ function generateDockerfile() {
1129
+ return `FROM node:18-alpine
1130
+
1131
+ WORKDIR /app
1132
+
1133
+ COPY package*.json ./
1134
+ RUN npm install --production
1135
+
1136
+ COPY . .
1137
+
1138
+ EXPOSE 3000
1139
+
1140
+ CMD ["node", "src/index.js"]
1141
+ `;
1142
+ }
1143
+ function generateEnvExample(config) {
1144
+ let env = `# Database
1145
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/myapp
1146
+
1147
+ # Server
1148
+ PORT=3000
1149
+ NODE_ENV=development
1150
+
1151
+ # Authentication
1152
+ JWT_SECRET=your-secret-key-change-in-production
1153
+ JWT_EXPIRES_IN=7d
1154
+ `;
1155
+ if (config.chatOption === 'openai') {
1156
+ env += `
1157
+ # OpenAI (for AI chat)
1158
+ OPENAI_API_KEY=sk-...
1159
+ `;
1160
+ }
1161
+ return env;
1162
+ }
1163
+ function generateReadme(app, config) {
1164
+ return `# ${app?.name || 'ThinkSoft'} Backend
1165
+
1166
+ Auto-generated backend by ThinkSoft CLI.
1167
+
1168
+ ## Quick Start
1169
+
1170
+ \`\`\`bash
1171
+ # Install dependencies
1172
+ npm install
1173
+
1174
+ # Set up environment
1175
+ cp .env.example .env
1176
+ # Edit .env with your database credentials
1177
+
1178
+ # Set up database
1179
+ npm run db:setup
1180
+
1181
+ # Start server
1182
+ npm start
1183
+ \`\`\`
1184
+
1185
+ ## Docker
1186
+
1187
+ \`\`\`bash
1188
+ docker-compose up -d
1189
+ \`\`\`
1190
+
1191
+ ## API Endpoints
1192
+
1193
+ Base URL: \`http://localhost:3000${config.baseUrl}\`
1194
+
1195
+ See \`api.config.json\` for all endpoints.
1196
+
1197
+ ## Frontend Client
1198
+
1199
+ Copy \`client/api.js\` to your frontend project:
1200
+
1201
+ \`\`\`javascript
1202
+ import api from './api';
1203
+
1204
+ // List records
1205
+ const orders = await api.orders.list();
1206
+
1207
+ // Create record
1208
+ await api.orders.create({ ... });
1209
+ \`\`\`
1210
+ `;
1211
+ }
1212
+ function generatePostgresReadme(app) {
1213
+ return `# ${app?.name || 'ThinkSoft'} Database Schema
1214
+
1215
+ Auto-generated PostgreSQL schema by ThinkSoft CLI.
1216
+
1217
+ ## Setup
1218
+
1219
+ \`\`\`bash
1220
+ # Run schema
1221
+ psql -d your_database -f database/schema.sql
1222
+
1223
+ # Run triggers (if included)
1224
+ psql -d your_database -f database/triggers.sql
1225
+ \`\`\`
1226
+ `;
1227
+ }
1228
+ //# sourceMappingURL=export.js.map