@thinksoftai/cli 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1038 @@
1
+ "use strict";
2
+ /**
3
+ * Frontend command - Scaffold frontend projects for external AI tools
4
+ *
5
+ * Usage:
6
+ * thinksoft frontend --init # Scaffold new frontend
7
+ * thinksoft frontend --init --app ID # Scaffold for specific app
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ var __importDefault = (this && this.__importDefault) || function (mod) {
43
+ return (mod && mod.__esModule) ? mod : { "default": mod };
44
+ };
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.frontend = frontend;
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ const chalk_1 = __importDefault(require("chalk"));
50
+ const ora_1 = __importDefault(require("ora"));
51
+ const inquirer_1 = __importDefault(require("inquirer"));
52
+ const config = __importStar(require("../utils/config"));
53
+ const api = __importStar(require("../utils/api"));
54
+ async function frontend(options) {
55
+ if (!options.init) {
56
+ console.log(chalk_1.default.yellow('\nUsage: thinksoft frontend --init [--app <appId>]'));
57
+ console.log(chalk_1.default.gray(' Scaffold a new frontend project\n'));
58
+ return;
59
+ }
60
+ if (!config.isLoggedIn()) {
61
+ console.log(chalk_1.default.red('\n✗ Not logged in'));
62
+ console.log(chalk_1.default.gray(' Run: thinksoft login\n'));
63
+ return;
64
+ }
65
+ let appId = options.app;
66
+ // If no app specified, let user select
67
+ if (!appId) {
68
+ const appsResult = await api.listApps();
69
+ if (appsResult.error || !appsResult.apps?.length) {
70
+ console.log(chalk_1.default.red('\n✗ No apps found'));
71
+ console.log(chalk_1.default.gray(' Run: thinksoft create "Your app description"\n'));
72
+ return;
73
+ }
74
+ const { selectedApp } = await inquirer_1.default.prompt([{
75
+ type: 'list',
76
+ name: 'selectedApp',
77
+ message: 'Select an app:',
78
+ choices: appsResult.apps.map((a) => ({
79
+ name: `${a.name} (${a.short_id})`,
80
+ value: a.short_id
81
+ }))
82
+ }]);
83
+ appId = selectedApp;
84
+ }
85
+ console.log();
86
+ console.log(chalk_1.default.cyan('┌─────────────────────────────────────────────────────────────┐'));
87
+ console.log(chalk_1.default.cyan('│') + chalk_1.default.white.bold(' ThinkSoft Frontend ') + chalk_1.default.cyan('│'));
88
+ console.log(chalk_1.default.cyan('│') + chalk_1.default.gray(` App: ${appId}`.padEnd(61)) + chalk_1.default.cyan('│'));
89
+ console.log(chalk_1.default.cyan('└─────────────────────────────────────────────────────────────┘'));
90
+ console.log();
91
+ // Fetch app info and schema
92
+ const spinner = (0, ora_1.default)('Fetching app schema...').start();
93
+ const appInfo = await fetchAppInfo(appId);
94
+ if (!appInfo) {
95
+ spinner.fail('Failed to fetch app info');
96
+ return;
97
+ }
98
+ spinner.succeed(`Found ${appInfo.tables.length} tables`);
99
+ console.log();
100
+ // Ask for project name
101
+ const { projectName } = await inquirer_1.default.prompt([{
102
+ type: 'input',
103
+ name: 'projectName',
104
+ message: 'Project folder name:',
105
+ default: `${appInfo.name.toLowerCase().replace(/\s+/g, '-')}-frontend`
106
+ }]);
107
+ // Scaffold the project
108
+ console.log();
109
+ await scaffoldFrontend(appId, appInfo, projectName);
110
+ // Show next steps
111
+ showNextSteps(projectName);
112
+ }
113
+ async function fetchAppInfo(appId) {
114
+ try {
115
+ // Get app details
116
+ const appResult = await api.getApp(appId);
117
+ if (appResult.error)
118
+ return null;
119
+ // Get tables/schema
120
+ const tablesResult = await api.getTables(appId);
121
+ const tables = [];
122
+ if (tablesResult.tables) {
123
+ for (const table of tablesResult.tables) {
124
+ const columns = (table.columns || []).map((col) => ({
125
+ name: col.name,
126
+ type: col.type || 'text',
127
+ required: col.required || false
128
+ }));
129
+ tables.push({ name: table.name, columns });
130
+ }
131
+ }
132
+ return {
133
+ name: appResult.app?.name || appId,
134
+ short_id: appId,
135
+ description: appResult.app?.description || appResult.app?.prompt || undefined,
136
+ tables
137
+ };
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ async function scaffoldFrontend(appId, appInfo, projectName) {
144
+ const projectDir = path.join(process.cwd(), projectName);
145
+ console.log(chalk_1.default.cyan(`📦 Scaffolding: ${chalk_1.default.white.bold(projectName)}/`));
146
+ console.log();
147
+ // Create project directory
148
+ if (!fs.existsSync(projectDir)) {
149
+ fs.mkdirSync(projectDir, { recursive: true });
150
+ }
151
+ // Create package.json
152
+ const packageJson = {
153
+ name: projectName,
154
+ version: '0.1.0',
155
+ private: true,
156
+ type: 'module',
157
+ scripts: {
158
+ dev: 'vite',
159
+ build: 'tsc && vite build',
160
+ preview: 'vite preview',
161
+ deploy: `thinksoft deploy --app ${appId}`
162
+ },
163
+ dependencies: {
164
+ '@thinksoftai/sdk': '^1.0.0',
165
+ 'react': '^18.2.0',
166
+ 'react-dom': '^18.2.0',
167
+ 'react-router-dom': '^6.20.0',
168
+ 'lucide-react': '^0.294.0'
169
+ },
170
+ devDependencies: {
171
+ '@types/react': '^18.2.0',
172
+ '@types/react-dom': '^18.2.0',
173
+ '@vitejs/plugin-react': '^4.2.0',
174
+ 'autoprefixer': '^10.4.16',
175
+ 'postcss': '^8.4.32',
176
+ 'tailwindcss': '^3.3.6',
177
+ 'typescript': '^5.3.0',
178
+ 'vite': '^5.0.0'
179
+ }
180
+ };
181
+ writeFile(projectDir, 'package.json', JSON.stringify(packageJson, null, 2));
182
+ // Create thinksoft.json
183
+ const thinksoftJson = {
184
+ appId,
185
+ name: appInfo.name,
186
+ framework: 'react',
187
+ outputDir: 'dist'
188
+ };
189
+ writeFile(projectDir, 'thinksoft.json', JSON.stringify(thinksoftJson, null, 2));
190
+ // Create vite.config.ts
191
+ writeFile(projectDir, 'vite.config.ts', `import { defineConfig } from 'vite'
192
+ import react from '@vitejs/plugin-react'
193
+ import path from 'path'
194
+
195
+ export default defineConfig({
196
+ plugins: [react()],
197
+ resolve: {
198
+ alias: {
199
+ '@': path.resolve(__dirname, './src'),
200
+ },
201
+ },
202
+ })
203
+ `);
204
+ // Create tsconfig.json
205
+ const tsConfig = {
206
+ compilerOptions: {
207
+ target: 'ES2020',
208
+ useDefineForClassFields: true,
209
+ lib: ['ES2020', 'DOM', 'DOM.Iterable'],
210
+ module: 'ESNext',
211
+ skipLibCheck: true,
212
+ moduleResolution: 'bundler',
213
+ allowImportingTsExtensions: true,
214
+ resolveJsonModule: true,
215
+ isolatedModules: true,
216
+ noEmit: true,
217
+ jsx: 'react-jsx',
218
+ strict: true,
219
+ noUnusedLocals: true,
220
+ noUnusedParameters: true,
221
+ noFallthroughCasesInSwitch: true,
222
+ baseUrl: '.',
223
+ paths: { '@/*': ['./src/*'] }
224
+ },
225
+ include: ['src'],
226
+ references: [{ path: './tsconfig.node.json' }]
227
+ };
228
+ writeFile(projectDir, 'tsconfig.json', JSON.stringify(tsConfig, null, 2));
229
+ // Create tsconfig.node.json
230
+ writeFile(projectDir, 'tsconfig.node.json', JSON.stringify({
231
+ compilerOptions: {
232
+ composite: true,
233
+ skipLibCheck: true,
234
+ module: 'ESNext',
235
+ moduleResolution: 'bundler',
236
+ allowSyntheticDefaultImports: true
237
+ },
238
+ include: ['vite.config.ts']
239
+ }, null, 2));
240
+ // Create tailwind.config.js
241
+ writeFile(projectDir, 'tailwind.config.js', `/** @type {import('tailwindcss').Config} */
242
+ export default {
243
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
244
+ theme: { extend: {} },
245
+ plugins: [],
246
+ }
247
+ `);
248
+ // Create postcss.config.js
249
+ writeFile(projectDir, 'postcss.config.js', `export default {
250
+ plugins: {
251
+ tailwindcss: {},
252
+ autoprefixer: {},
253
+ },
254
+ }
255
+ `);
256
+ // Create index.html
257
+ writeFile(projectDir, 'index.html', `<!DOCTYPE html>
258
+ <html lang="en">
259
+ <head>
260
+ <meta charset="UTF-8" />
261
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
262
+ <title>${appInfo.name}</title>
263
+ </head>
264
+ <body>
265
+ <div id="root"></div>
266
+ <script type="module" src="/src/main.tsx"></script>
267
+ </body>
268
+ </html>
269
+ `);
270
+ // Create src directory structure
271
+ const srcDir = path.join(projectDir, 'src');
272
+ const libDir = path.join(srcDir, 'lib');
273
+ const contextDir = path.join(srcDir, 'context');
274
+ const componentsDir = path.join(srcDir, 'components');
275
+ const pagesDir = path.join(srcDir, 'pages');
276
+ fs.mkdirSync(libDir, { recursive: true });
277
+ fs.mkdirSync(contextDir, { recursive: true });
278
+ fs.mkdirSync(componentsDir, { recursive: true });
279
+ fs.mkdirSync(pagesDir, { recursive: true });
280
+ // Create src/main.tsx
281
+ writeFile(srcDir, 'main.tsx', `import React from 'react'
282
+ import ReactDOM from 'react-dom/client'
283
+ import { BrowserRouter } from 'react-router-dom'
284
+ import App from './App'
285
+ import './index.css'
286
+
287
+ ReactDOM.createRoot(document.getElementById('root')!).render(
288
+ <React.StrictMode>
289
+ <BrowserRouter>
290
+ <App />
291
+ </BrowserRouter>
292
+ </React.StrictMode>,
293
+ )
294
+ `);
295
+ // Create src/index.css
296
+ writeFile(srcDir, 'index.css', `@tailwind base;
297
+ @tailwind components;
298
+ @tailwind utilities;
299
+ `);
300
+ // Create src/lib/client.ts
301
+ writeFile(libDir, 'client.ts', `import { ThinkSoft } from '@thinksoftai/sdk'
302
+
303
+ export const client = new ThinkSoft({ appId: '${appId}' })
304
+ `);
305
+ // Create src/context/AuthContext.tsx
306
+ writeFile(contextDir, 'AuthContext.tsx', `import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'
307
+ import { client } from '../lib/client'
308
+
309
+ type AuthContextType = {
310
+ isAuthenticated: boolean
311
+ user: { email: string } | null
312
+ login: (email: string, otp: string) => Promise<boolean>
313
+ sendOTP: (email: string) => Promise<boolean>
314
+ logout: () => void
315
+ showLoginModal: boolean
316
+ setShowLoginModal: (show: boolean) => void
317
+ requireAuth: (callback: () => void) => void
318
+ }
319
+
320
+ const AuthContext = createContext<AuthContextType | null>(null)
321
+
322
+ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
323
+ const [isAuthenticated, setIsAuthenticated] = useState(false)
324
+ const [user, setUser] = useState<{ email: string } | null>(null)
325
+ const [showLoginModal, setShowLoginModal] = useState(false)
326
+ const [pendingAction, setPendingAction] = useState<(() => void) | null>(null)
327
+
328
+ useEffect(() => {
329
+ const authed = client.auth.isAuthenticated()
330
+ setIsAuthenticated(authed)
331
+ if (authed) {
332
+ setUser({ email: localStorage.getItem('ts_user_email') || '' })
333
+ }
334
+ }, [])
335
+
336
+ const sendOTP = async (email: string): Promise<boolean> => {
337
+ try {
338
+ await client.auth.sendOTP(email)
339
+ localStorage.setItem('ts_user_email', email)
340
+ return true
341
+ } catch {
342
+ return false
343
+ }
344
+ }
345
+
346
+ const login = async (email: string, otp: string): Promise<boolean> => {
347
+ try {
348
+ await client.auth.verifyOTP(email, otp)
349
+ setIsAuthenticated(true)
350
+ setUser({ email })
351
+ setShowLoginModal(false)
352
+ if (pendingAction) {
353
+ pendingAction()
354
+ setPendingAction(null)
355
+ }
356
+ return true
357
+ } catch {
358
+ return false
359
+ }
360
+ }
361
+
362
+ const logout = () => {
363
+ client.auth.logout()
364
+ setIsAuthenticated(false)
365
+ setUser(null)
366
+ localStorage.removeItem('ts_user_email')
367
+ }
368
+
369
+ const requireAuth = (callback: () => void) => {
370
+ if (isAuthenticated) {
371
+ callback()
372
+ } else {
373
+ setPendingAction(() => callback)
374
+ setShowLoginModal(true)
375
+ }
376
+ }
377
+
378
+ return (
379
+ <AuthContext.Provider value={{
380
+ isAuthenticated, user, login, sendOTP, logout,
381
+ showLoginModal, setShowLoginModal, requireAuth
382
+ }}>
383
+ {children}
384
+ </AuthContext.Provider>
385
+ )
386
+ }
387
+
388
+ export const useAuth = () => {
389
+ const ctx = useContext(AuthContext)
390
+ if (!ctx) throw new Error('useAuth must be used within AuthProvider')
391
+ return ctx
392
+ }
393
+ `);
394
+ // Create src/components/LoginModal.tsx
395
+ writeFile(componentsDir, 'LoginModal.tsx', `import React, { useState } from 'react'
396
+ import { useAuth } from '../context/AuthContext'
397
+ import { X } from 'lucide-react'
398
+
399
+ const LoginModal: React.FC = () => {
400
+ const { sendOTP, login, showLoginModal, setShowLoginModal } = useAuth()
401
+ const [email, setEmail] = useState('')
402
+ const [otp, setOtp] = useState('')
403
+ const [step, setStep] = useState<'email' | 'otp'>('email')
404
+ const [loading, setLoading] = useState(false)
405
+ const [error, setError] = useState('')
406
+
407
+ if (!showLoginModal) return null
408
+
409
+ const handleSendOTP = async (e: React.FormEvent) => {
410
+ e.preventDefault()
411
+ setLoading(true)
412
+ setError('')
413
+ const ok = await sendOTP(email)
414
+ setLoading(false)
415
+ if (ok) setStep('otp')
416
+ else setError('Failed to send OTP')
417
+ }
418
+
419
+ const handleVerify = async (e: React.FormEvent) => {
420
+ e.preventDefault()
421
+ setLoading(true)
422
+ setError('')
423
+ const ok = await login(email, otp)
424
+ setLoading(false)
425
+ if (!ok) setError('Invalid OTP')
426
+ }
427
+
428
+ const handleClose = () => {
429
+ setShowLoginModal(false)
430
+ setStep('email')
431
+ setError('')
432
+ }
433
+
434
+ return (
435
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
436
+ <div className="bg-white rounded-xl p-6 w-full max-w-md relative">
437
+ <button onClick={handleClose} className="absolute top-4 right-4 text-gray-500 hover:text-gray-700">
438
+ <X size={20} />
439
+ </button>
440
+ <h2 className="text-xl font-bold mb-4">Login Required</h2>
441
+ <p className="text-sm text-gray-600 mb-4">Please login to perform this action.</p>
442
+ {error && <p className="text-red-500 text-sm mb-4">{error}</p>}
443
+ {step === 'email' ? (
444
+ <form onSubmit={handleSendOTP}>
445
+ <input
446
+ type="email"
447
+ value={email}
448
+ onChange={e => setEmail(e.target.value)}
449
+ placeholder="Email"
450
+ className="w-full p-3 border rounded mb-4"
451
+ required
452
+ />
453
+ <button
454
+ type="submit"
455
+ disabled={loading}
456
+ className="w-full bg-blue-600 text-white p-3 rounded hover:bg-blue-700 disabled:opacity-50"
457
+ >
458
+ {loading ? 'Sending...' : 'Send OTP'}
459
+ </button>
460
+ </form>
461
+ ) : (
462
+ <form onSubmit={handleVerify}>
463
+ <p className="text-sm text-gray-600 mb-4">OTP sent to {email}</p>
464
+ <input
465
+ type="text"
466
+ value={otp}
467
+ onChange={e => setOtp(e.target.value)}
468
+ placeholder="Enter OTP"
469
+ className="w-full p-3 border rounded mb-4"
470
+ required
471
+ />
472
+ <button
473
+ type="submit"
474
+ disabled={loading}
475
+ className="w-full bg-blue-600 text-white p-3 rounded hover:bg-blue-700 disabled:opacity-50"
476
+ >
477
+ {loading ? 'Verifying...' : 'Verify'}
478
+ </button>
479
+ <button
480
+ type="button"
481
+ onClick={() => setStep('email')}
482
+ className="w-full mt-2 text-gray-600 text-sm"
483
+ >
484
+ Back
485
+ </button>
486
+ </form>
487
+ )}
488
+ </div>
489
+ </div>
490
+ )
491
+ }
492
+
493
+ export default LoginModal
494
+ `);
495
+ // Create src/App.tsx (minimal shell)
496
+ writeFile(srcDir, 'App.tsx', `import React from 'react'
497
+ import { Routes, Route } from 'react-router-dom'
498
+ import { AuthProvider } from './context/AuthContext'
499
+ import LoginModal from './components/LoginModal'
500
+
501
+ const App: React.FC = () => {
502
+ return (
503
+ <AuthProvider>
504
+ <div className="min-h-screen bg-gray-50">
505
+ <main className="p-6">
506
+ <Routes>
507
+ <Route path="/" element={<HomePage />} />
508
+ {/* Add your routes here */}
509
+ </Routes>
510
+ </main>
511
+ <LoginModal />
512
+ </div>
513
+ </AuthProvider>
514
+ )
515
+ }
516
+
517
+ // Placeholder home page - replace with your own
518
+ const HomePage: React.FC = () => {
519
+ return (
520
+ <div className="max-w-4xl mx-auto">
521
+ <h1 className="text-3xl font-bold text-gray-900 mb-4">${appInfo.name}</h1>
522
+ <p className="text-gray-600 mb-8">Your frontend is ready. Start building!</p>
523
+
524
+ <div className="bg-white rounded-xl border border-gray-200 p-6">
525
+ <h2 className="text-lg font-semibold mb-4">Getting Started</h2>
526
+ <ol className="list-decimal list-inside space-y-2 text-gray-700">
527
+ <li>Read <code className="bg-gray-100 px-2 py-1 rounded text-sm">CONTEXT.md</code> for backend schema</li>
528
+ <li>Create pages in <code className="bg-gray-100 px-2 py-1 rounded text-sm">src/pages/</code></li>
529
+ <li>Add routes to <code className="bg-gray-100 px-2 py-1 rounded text-sm">src/App.tsx</code></li>
530
+ <li>Use Cursor or Claude to VIBE CODE your pages</li>
531
+ </ol>
532
+ </div>
533
+ </div>
534
+ )
535
+ }
536
+
537
+ export default App
538
+ `);
539
+ // Create .gitignore
540
+ writeFile(projectDir, '.gitignore', `node_modules
541
+ dist
542
+ .env
543
+ .env.local
544
+ *.log
545
+ `);
546
+ // Create CONTEXT.md for AI tools
547
+ const contextMd = generateContextMd(appId, appInfo);
548
+ writeFile(projectDir, 'CONTEXT.md', contextMd);
549
+ // Create .cursorrules
550
+ const cursorrules = generateCursorrules(appId, appInfo);
551
+ writeFile(projectDir, '.cursorrules', cursorrules);
552
+ console.log();
553
+ console.log(chalk_1.default.green('✅ Frontend scaffolded successfully!'));
554
+ }
555
+ function writeFile(dir, filename, content) {
556
+ const filePath = path.join(dir, filename);
557
+ fs.writeFileSync(filePath, content);
558
+ console.log(chalk_1.default.gray(` ${filename}`));
559
+ }
560
+ function generateContextMd(appId, appInfo) {
561
+ // Generate table schema section
562
+ const tablesSection = appInfo.tables.length > 0
563
+ ? appInfo.tables.map(table => {
564
+ const columnsTable = table.columns.length > 0
565
+ ? table.columns.map(col => `| ${col.name} | ${col.type} | ${col.required ? 'Yes' : 'No'} |`).join('\n')
566
+ : '| (no columns defined) | - | - |';
567
+ return `### \`${table.name}\`
568
+
569
+ | Column | Type | Required |
570
+ |--------|------|----------|
571
+ ${columnsTable}`;
572
+ }).join('\n\n')
573
+ : '*No tables defined yet. Create tables in the ThinkSoft dashboard.*';
574
+ // Generate SDK examples with actual table names
575
+ const exampleTable = appInfo.tables[0]?.name || 'items';
576
+ const exampleColumns = appInfo.tables[0]?.columns || [];
577
+ const exampleField = exampleColumns[0]?.name || 'name';
578
+ // Build app info table with optional description
579
+ const appInfoRows = [
580
+ `| App ID | \`${appId}\` |`,
581
+ `| App Name | ${appInfo.name} |`
582
+ ];
583
+ if (appInfo.description) {
584
+ appInfoRows.push(`| Description | ${appInfo.description} |`);
585
+ }
586
+ appInfoRows.push(`| Tables | ${appInfo.tables.length} |`);
587
+ return `# ${appInfo.name} - ThinkSoft Backend Context
588
+
589
+ > This file provides context for AI-assisted development (Cursor, Claude, Copilot, etc.)
590
+ > Read this to understand the backend schema and SDK usage.
591
+
592
+ ## App Information
593
+
594
+ | Property | Value |
595
+ |----------|-------|
596
+ ${appInfoRows.join('\n')}
597
+
598
+ ---
599
+
600
+ ## Database Schema
601
+
602
+ ${tablesSection}
603
+
604
+ ---
605
+
606
+ ## ThinkSoft SDK Reference
607
+
608
+ The SDK client is pre-configured in \`src/lib/client.ts\`.
609
+
610
+ ### Import
611
+
612
+ \`\`\`typescript
613
+ import { client } from '../lib/client'
614
+ \`\`\`
615
+
616
+ ### List Records
617
+
618
+ \`\`\`typescript
619
+ // Get all records
620
+ const { data } = await client.${exampleTable}.list()
621
+
622
+ // With filtering
623
+ const { data } = await client.${exampleTable}.list({
624
+ filter: { status: 'active' }
625
+ })
626
+
627
+ // With sorting (field:asc or field:desc)
628
+ const { data } = await client.${exampleTable}.list({
629
+ sort: 'created_at:desc'
630
+ })
631
+
632
+ // With pagination
633
+ const { data } = await client.${exampleTable}.list({
634
+ limit: 10,
635
+ offset: 0
636
+ })
637
+
638
+ // Combined
639
+ const { data } = await client.${exampleTable}.list({
640
+ filter: { status: 'active' },
641
+ sort: 'created_at:desc',
642
+ limit: 20
643
+ })
644
+ \`\`\`
645
+
646
+ ### Get Single Record
647
+
648
+ \`\`\`typescript
649
+ const record = await client.${exampleTable}.get('record-id')
650
+ \`\`\`
651
+
652
+ ### Create Record
653
+
654
+ \`\`\`typescript
655
+ const newRecord = await client.${exampleTable}.create({
656
+ ${exampleField}: 'value'
657
+ // add other fields...
658
+ })
659
+ \`\`\`
660
+
661
+ ### Update Record
662
+
663
+ \`\`\`typescript
664
+ await client.${exampleTable}.update('record-id', {
665
+ ${exampleField}: 'new value'
666
+ })
667
+ \`\`\`
668
+
669
+ ### Delete Record
670
+
671
+ \`\`\`typescript
672
+ await client.${exampleTable}.delete('record-id')
673
+ \`\`\`
674
+
675
+ ---
676
+
677
+ ## Schema Management (Create Tables & Columns)
678
+
679
+ The SDK can create and modify tables/columns programmatically.
680
+
681
+ ### Create a New Table
682
+
683
+ \`\`\`typescript
684
+ await client.schema.createTable({
685
+ name: 'Reviews',
686
+ icon: '⭐',
687
+ columns: [
688
+ { name: 'rating', type: 'number', required: true },
689
+ { name: 'comment', type: 'textarea' },
690
+ { name: 'author', type: 'text' }
691
+ ]
692
+ })
693
+ \`\`\`
694
+
695
+ ### Add Column to Existing Table
696
+
697
+ \`\`\`typescript
698
+ await client.schema.addColumn('${exampleTable}', {
699
+ name: 'priority',
700
+ type: 'select',
701
+ options: ['low', 'medium', 'high']
702
+ })
703
+ \`\`\`
704
+
705
+ ### Update Column
706
+
707
+ \`\`\`typescript
708
+ await client.schema.updateColumn('${exampleTable}', 'priority', {
709
+ options: ['low', 'medium', 'high', 'urgent']
710
+ })
711
+ \`\`\`
712
+
713
+ ### Delete Column
714
+
715
+ \`\`\`typescript
716
+ await client.schema.deleteColumn('${exampleTable}', 'old_field')
717
+ \`\`\`
718
+
719
+ ### Delete Table
720
+
721
+ \`\`\`typescript
722
+ await client.schema.deleteTable('old_table')
723
+ \`\`\`
724
+
725
+ ### Column Types
726
+
727
+ | Type | Description |
728
+ |------|-------------|
729
+ | text | Single line text |
730
+ | email | Email address |
731
+ | phone | Phone number |
732
+ | number | Numeric value |
733
+ | date | Date picker |
734
+ | url | URL/link |
735
+ | textarea | Multi-line text |
736
+ | richtext | Rich text editor |
737
+ | select | Single select dropdown |
738
+ | multiselect | Multi-select |
739
+ | radio | Radio buttons |
740
+ | checkbox | Checkbox |
741
+ | file | File upload |
742
+ | reference | Link to another table |
743
+
744
+ ---
745
+
746
+ ## Authentication Pattern (Auth-on-Action)
747
+
748
+ Users can **browse freely without login**. Authentication is only required for write operations (create, update, delete).
749
+
750
+ ### Using requireAuth()
751
+
752
+ \`\`\`typescript
753
+ import { useAuth } from '../context/AuthContext'
754
+ import { client } from '../lib/client'
755
+
756
+ const MyComponent = () => {
757
+ const { requireAuth, isAuthenticated, user } = useAuth()
758
+ const [items, setItems] = useState([])
759
+
760
+ // READ: No auth required
761
+ useEffect(() => {
762
+ client.${exampleTable}.list().then(({ data }) => setItems(data))
763
+ }, [])
764
+
765
+ // WRITE: Wrap with requireAuth - shows login modal if needed
766
+ const handleCreate = () => {
767
+ requireAuth(async () => {
768
+ await client.${exampleTable}.create({ ${exampleField}: 'New Item' })
769
+ // Refresh data after create
770
+ const { data } = await client.${exampleTable}.list()
771
+ setItems(data)
772
+ })
773
+ }
774
+
775
+ const handleUpdate = (id: string) => {
776
+ requireAuth(async () => {
777
+ await client.${exampleTable}.update(id, { ${exampleField}: 'Updated' })
778
+ })
779
+ }
780
+
781
+ const handleDelete = (id: string) => {
782
+ requireAuth(async () => {
783
+ await client.${exampleTable}.delete(id)
784
+ setItems(items.filter(item => item.id !== id))
785
+ })
786
+ }
787
+
788
+ return (
789
+ <div>
790
+ {isAuthenticated && <p>Logged in as {user?.email}</p>}
791
+ <button onClick={handleCreate}>Add Item</button>
792
+ {items.map(item => (
793
+ <div key={item.id}>
794
+ {item.${exampleField}}
795
+ <button onClick={() => handleDelete(item.id)}>Delete</button>
796
+ </div>
797
+ ))}
798
+ </div>
799
+ )
800
+ }
801
+ \`\`\`
802
+
803
+ ---
804
+
805
+ ## Project Structure
806
+
807
+ \`\`\`
808
+ src/
809
+ ├── lib/
810
+ │ └── client.ts # ThinkSoft SDK client (configured)
811
+ ├── context/
812
+ │ └── AuthContext.tsx # Auth state + requireAuth()
813
+ ├── components/
814
+ │ └── LoginModal.tsx # Login modal (implemented)
815
+ ├── pages/ # Your pages go here
816
+ │ └── (create your pages)
817
+ ├── App.tsx # Router setup
818
+ ├── main.tsx # Entry point
819
+ └── index.css # Tailwind CSS
820
+ \`\`\`
821
+
822
+ ---
823
+
824
+ ## Tech Stack
825
+
826
+ - **React 18** + TypeScript
827
+ - **Vite** - Build tool
828
+ - **Tailwind CSS** - Styling
829
+ - **React Router v6** - Routing
830
+ - **Lucide React** - Icons
831
+ - **@thinksoftai/sdk** - Backend SDK
832
+
833
+ ---
834
+
835
+ ## Development Commands
836
+
837
+ \`\`\`bash
838
+ npm install # Install dependencies
839
+ npm run dev # Start dev server (http://localhost:5173)
840
+ npm run build # Build for production
841
+ npm run deploy # Deploy to ThinkSoft
842
+ \`\`\`
843
+
844
+ ---
845
+
846
+ ## Example: Complete Page
847
+
848
+ \`\`\`typescript
849
+ // src/pages/${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Page.tsx
850
+ import React, { useEffect, useState } from 'react'
851
+ import { client } from '../lib/client'
852
+ import { useAuth } from '../context/AuthContext'
853
+ import { Plus, Trash2 } from 'lucide-react'
854
+
855
+ interface ${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Item {
856
+ id: string
857
+ ${exampleField}: string
858
+ created_at: string
859
+ }
860
+
861
+ const ${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Page: React.FC = () => {
862
+ const [items, setItems] = useState<${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Item[]>([])
863
+ const [loading, setLoading] = useState(true)
864
+ const { requireAuth } = useAuth()
865
+
866
+ useEffect(() => {
867
+ loadItems()
868
+ }, [])
869
+
870
+ const loadItems = async () => {
871
+ setLoading(true)
872
+ const { data } = await client.${exampleTable}.list({ sort: 'created_at:desc' })
873
+ setItems(data || [])
874
+ setLoading(false)
875
+ }
876
+
877
+ const handleAdd = () => {
878
+ requireAuth(async () => {
879
+ await client.${exampleTable}.create({ ${exampleField}: 'New Item' })
880
+ loadItems()
881
+ })
882
+ }
883
+
884
+ const handleDelete = (id: string) => {
885
+ requireAuth(async () => {
886
+ await client.${exampleTable}.delete(id)
887
+ setItems(items.filter(item => item.id !== id))
888
+ })
889
+ }
890
+
891
+ if (loading) {
892
+ return <div className="p-6">Loading...</div>
893
+ }
894
+
895
+ return (
896
+ <div className="max-w-4xl mx-auto p-6">
897
+ <div className="flex justify-between items-center mb-6">
898
+ <h1 className="text-2xl font-bold">${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}</h1>
899
+ <button
900
+ onClick={handleAdd}
901
+ className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
902
+ >
903
+ <Plus size={20} />
904
+ Add New
905
+ </button>
906
+ </div>
907
+
908
+ <div className="bg-white rounded-xl border border-gray-200">
909
+ {items.length === 0 ? (
910
+ <div className="p-8 text-center text-gray-500">
911
+ No items yet. Click "Add New" to create one.
912
+ </div>
913
+ ) : (
914
+ <ul className="divide-y divide-gray-200">
915
+ {items.map(item => (
916
+ <li key={item.id} className="flex items-center justify-between p-4">
917
+ <span>{item.${exampleField}}</span>
918
+ <button
919
+ onClick={() => handleDelete(item.id)}
920
+ className="text-red-500 hover:text-red-700"
921
+ >
922
+ <Trash2 size={18} />
923
+ </button>
924
+ </li>
925
+ ))}
926
+ </ul>
927
+ )}
928
+ </div>
929
+ </div>
930
+ )
931
+ }
932
+
933
+ export default ${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Page
934
+ \`\`\`
935
+
936
+ Then add to App.tsx:
937
+ \`\`\`typescript
938
+ import ${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Page from './pages/${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Page'
939
+
940
+ // In Routes:
941
+ <Route path="/${exampleTable}" element={<${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Page />} />
942
+ \`\`\`
943
+ `;
944
+ }
945
+ function generateCursorrules(appId, appInfo) {
946
+ const tableNames = appInfo.tables.map(t => t.name).join(', ') || 'No tables yet';
947
+ const exampleTable = appInfo.tables[0]?.name || 'items';
948
+ return `# Cursor Rules for ${appInfo.name}
949
+
950
+ You are building a React frontend for a ThinkSoft backend app.
951
+
952
+ ## App Context
953
+ - App ID: \`${appId}\`
954
+ - Tables: ${tableNames}
955
+ - See CONTEXT.md for full schema details
956
+
957
+ ## Key Files (Already Configured)
958
+ - \`src/lib/client.ts\` - SDK client
959
+ - \`src/context/AuthContext.tsx\` - Auth with requireAuth()
960
+ - \`src/components/LoginModal.tsx\` - Login modal
961
+
962
+ ## Code Patterns
963
+
964
+ ### SDK Usage
965
+ \`\`\`typescript
966
+ import { client } from '../lib/client'
967
+
968
+ // CRUD operations
969
+ const { data } = await client.${exampleTable}.list()
970
+ const { data } = await client.${exampleTable}.list({ filter: { status: 'active' }, sort: 'created_at:desc' })
971
+ const item = await client.${exampleTable}.get(id)
972
+ await client.${exampleTable}.create({ ... })
973
+ await client.${exampleTable}.update(id, { ... })
974
+ await client.${exampleTable}.delete(id)
975
+
976
+ // Schema management (create tables/columns)
977
+ await client.schema.createTable({ name: 'Reviews', columns: [...] })
978
+ await client.schema.addColumn('${exampleTable}', { name: 'priority', type: 'select', options: ['low', 'high'] })
979
+ \`\`\`
980
+
981
+ ### Auth-on-Action Pattern
982
+ \`\`\`typescript
983
+ import { useAuth } from '../context/AuthContext'
984
+
985
+ const { requireAuth, isAuthenticated, user } = useAuth()
986
+
987
+ // Read = no auth needed
988
+ const { data } = await client.${exampleTable}.list()
989
+
990
+ // Write = wrap with requireAuth
991
+ const handleSave = () => {
992
+ requireAuth(async () => {
993
+ await client.${exampleTable}.create({ ... })
994
+ })
995
+ }
996
+ \`\`\`
997
+
998
+ ## Rules
999
+
1000
+ 1. Use TypeScript with proper types
1001
+ 2. Use Tailwind CSS for all styling
1002
+ 3. Use Lucide React for icons
1003
+ 4. Use default exports for pages and components
1004
+ 5. Import client from '../lib/client' (never create new ThinkSoft instance)
1005
+ 6. Import useAuth from '../context/AuthContext'
1006
+ 7. Wrap all create/update/delete with requireAuth()
1007
+ 8. Read operations don't need auth
1008
+
1009
+ ## Do NOT
1010
+ - Create new ThinkSoft() instances
1011
+ - Add BrowserRouter (already in main.tsx)
1012
+ - Generate package.json or config files
1013
+ - Use inline styles
1014
+ - Block users from reading data
1015
+ - Use named exports for page components
1016
+ `;
1017
+ }
1018
+ function showNextSteps(projectName) {
1019
+ console.log();
1020
+ console.log(chalk_1.default.cyan('┌─────────────────────────────────────────────────────────────┐'));
1021
+ console.log(chalk_1.default.cyan('│') + chalk_1.default.white.bold(' Next Steps ') + chalk_1.default.cyan('│'));
1022
+ console.log(chalk_1.default.cyan('└─────────────────────────────────────────────────────────────┘'));
1023
+ console.log();
1024
+ console.log(chalk_1.default.white(` 1. cd ${projectName}`));
1025
+ console.log(chalk_1.default.white(' 2. npm install'));
1026
+ console.log(chalk_1.default.white(' 3. npm run dev'));
1027
+ console.log();
1028
+ console.log(chalk_1.default.gray(' Build pages with VIBE CODE:'));
1029
+ console.log(chalk_1.default.white(' 4. Open in Cursor or VS Code'));
1030
+ console.log(chalk_1.default.white(' 5. Read CONTEXT.md for thinksoft backend app'));
1031
+ console.log(chalk_1.default.white(' 6. VIBE CODE Frontend App'));
1032
+ console.log();
1033
+ console.log(chalk_1.default.gray(' Deploy:'));
1034
+ console.log(chalk_1.default.white(' 7. npm run build'));
1035
+ console.log(chalk_1.default.white(' 8. npm run deploy'));
1036
+ console.log();
1037
+ }
1038
+ //# sourceMappingURL=frontend.js.map