@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.
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +1228 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/index.js +27 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/api.d.ts +5 -0
- package/dist/utils/api.js +11 -0
- package/dist/utils/api.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|