create-tosijs-platform-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # create-tosijs-platform-app
2
+
3
+ Create a full-stack web app with [tosijs](https://github.com/nicross/tosijs) (front-end) and Google Firebase (back-end).
4
+
5
+ ## Prerequisites
6
+
7
+ - [Bun](https://bun.sh/) - Install with `curl -fsSL https://bun.sh/install | bash`
8
+ - [Firebase CLI](https://firebase.google.com/docs/cli) - Install with `bun install -g firebase-tools`
9
+ - A Firebase project (create one at [console.firebase.google.com](https://console.firebase.google.com))
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ npx create-tosijs-platform-app my-awesome-site
15
+ ```
16
+
17
+ The CLI will:
18
+ 1. Check prerequisites (Bun, Firebase CLI, Firebase login)
19
+ 2. Ask for your Firebase project ID and admin email*
20
+ 3. Clone the tosijs-platform template
21
+ 4. Configure Firebase settings automatically
22
+ 5. Install dependencies
23
+ 6. Generate TLS certificates for local HTTPS development
24
+
25
+ *The admin email is the Google account that will be granted owner/admin rights to your site. This email is only stored locally in your project's `setup.js` file and is never transmitted anywhere. It's used to identify which user should get admin privileges when you run `node setup.js` after deploying.
26
+
27
+ ## After Setup
28
+
29
+ 1. **Enable Firebase services** in your project:
30
+ - Authentication (Google sign-in)
31
+ - Firestore Database
32
+ - Cloud Storage
33
+
34
+ 2. **Upgrade to Blaze plan** (required for Cloud Functions)
35
+
36
+ 3. **Deploy**:
37
+ ```bash
38
+ cd my-awesome-site
39
+ bun deploy-functions
40
+ bun deploy-hosting
41
+ ```
42
+
43
+ 4. **Set up admin access**:
44
+ ```bash
45
+ node setup.js
46
+ ```
47
+
48
+ 5. **Start local development**:
49
+ ```bash
50
+ bun start
51
+ ```
52
+ Visit https://localhost:8020
53
+
54
+ ## AI Features (Optional)
55
+
56
+ To use the `/gen` endpoint for AI completions, set up API keys:
57
+
58
+ ```bash
59
+ # For Gemini (Google AI)
60
+ firebase functions:secrets:set gemini-api-key
61
+
62
+ # For ChatGPT (OpenAI)
63
+ firebase functions:secrets:set chatgpt-api-key
64
+ ```
65
+
66
+ ## Links
67
+
68
+ - [tosijs-platform template](https://github.com/tonioloewald/tosijs-platform)
69
+ - [Report issues](https://github.com/tonioloewald/tosijs-platform/issues)
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,743 @@
1
+ #! /usr/bin/env node
2
+
3
+ 'use strict'
4
+
5
+ import { execSync } from 'child_process'
6
+ import path from 'path'
7
+ import fs from 'fs'
8
+ import readline from 'readline'
9
+
10
+ const rl = readline.createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout,
13
+ })
14
+
15
+ function question(query) {
16
+ return new Promise((resolve) => rl.question(query, resolve))
17
+ }
18
+
19
+ function execCommand(command, options = {}) {
20
+ try {
21
+ return execSync(command, {
22
+ stdio: options.silent ? 'pipe' : 'inherit',
23
+ encoding: 'utf-8',
24
+ ...options,
25
+ })
26
+ } catch (error) {
27
+ if (!options.allowFailure) {
28
+ throw error
29
+ }
30
+ return null
31
+ }
32
+ }
33
+
34
+ function checkBun() {
35
+ try {
36
+ const version = execSync('bun --version', {
37
+ stdio: 'pipe',
38
+ encoding: 'utf-8',
39
+ }).trim()
40
+ console.log(`✓ Bun installed (${version})`)
41
+ return true
42
+ } catch (error) {
43
+ console.log('✗ Bun not found')
44
+ return false
45
+ }
46
+ }
47
+
48
+ function checkFirebaseCLI() {
49
+ try {
50
+ const version = execSync('firebase --version', {
51
+ stdio: 'pipe',
52
+ encoding: 'utf-8',
53
+ }).trim()
54
+ console.log(`✓ Firebase CLI installed (${version})`)
55
+ return true
56
+ } catch (error) {
57
+ console.log('✗ Firebase CLI not found')
58
+ return false
59
+ }
60
+ }
61
+
62
+ function checkFirebaseLogin() {
63
+ try {
64
+ const result = execSync('firebase login:list', {
65
+ stdio: 'pipe',
66
+ encoding: 'utf-8',
67
+ })
68
+ if (result.includes('No authorized accounts')) {
69
+ return false
70
+ }
71
+ console.log('✓ Firebase CLI is logged in')
72
+ return true
73
+ } catch (error) {
74
+ return false
75
+ }
76
+ }
77
+
78
+ function validateEmail(email) {
79
+ const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
80
+ return re.test(email)
81
+ }
82
+
83
+ function validateProjectId(projectId) {
84
+ const re = /^[a-z0-9-]+$/
85
+ return re.test(projectId) && projectId.length >= 6 && projectId.length <= 30
86
+ }
87
+
88
+ function getFirebaseConfig(projectId) {
89
+ try {
90
+ console.log('\n🔍 Fetching Firebase configuration...')
91
+ const appsJson = execSync(
92
+ `firebase apps:sdkconfig web --project ${projectId}`,
93
+ {
94
+ stdio: 'pipe',
95
+ encoding: 'utf-8',
96
+ }
97
+ )
98
+
99
+ const config = JSON.parse(appsJson)
100
+ if (config && config.projectId) {
101
+ console.log('✓ Successfully retrieved Firebase config')
102
+ return config
103
+ }
104
+ return null
105
+ } catch (error) {
106
+ console.log('⚠ Could not auto-fetch Firebase config')
107
+ if (error.message.includes('No apps found')) {
108
+ console.log(' You need to create a Web App in Firebase Console first.')
109
+ }
110
+ return null
111
+ }
112
+ }
113
+
114
+ function checkFirestoreExists(projectId) {
115
+ try {
116
+ execSync(`firebase firestore:databases:list --project ${projectId}`, {
117
+ stdio: 'pipe',
118
+ encoding: 'utf-8',
119
+ })
120
+ return true
121
+ } catch (error) {
122
+ return false
123
+ }
124
+ }
125
+
126
+ function checkBlazePlan(projectId) {
127
+ try {
128
+ // Try to get project info - Blaze plan info is in the project details
129
+ const projectInfo = execSync(`firebase projects:list --json`, {
130
+ stdio: 'pipe',
131
+ encoding: 'utf-8',
132
+ })
133
+
134
+ const projects = JSON.parse(projectInfo)
135
+ const project = projects.find((p) => p.projectId === projectId)
136
+
137
+ // If we can't determine, return null (unknown)
138
+ // The billing plan isn't directly in projects:list, so we'll try another approach
139
+
140
+ // Try to list functions - this will fail if not on Blaze plan
141
+ execSync(`firebase functions:list --project ${projectId}`, {
142
+ stdio: 'pipe',
143
+ encoding: 'utf-8',
144
+ })
145
+
146
+ // If the command succeeds, they likely have functions enabled (Blaze plan)
147
+ return true
148
+ } catch (error) {
149
+ // If error mentions billing or quota, definitely not on Blaze
150
+ if (
151
+ error.message.includes('billing') ||
152
+ error.message.includes('quota') ||
153
+ error.message.includes('upgrade') ||
154
+ error.message.includes('requires a paid plan')
155
+ ) {
156
+ return false
157
+ }
158
+ // Otherwise, we can't determine - return null
159
+ return null
160
+ }
161
+ }
162
+
163
+ async function main() {
164
+ console.log('\n🚀 Create tosijs-platform App\n')
165
+
166
+ if (process.argv.length < 3) {
167
+ console.log('Usage: npx create-tosijs-platform-app <project-name>')
168
+ console.log('Example: npx create-tosijs-platform-app my-awesome-site\n')
169
+ process.exit(1)
170
+ }
171
+
172
+ const projectName = process.argv[2]
173
+ const currentPath = process.cwd()
174
+ const projectPath = path.join(currentPath, projectName)
175
+ const gitRepo = 'https://github.com/tonioloewald/tosijs-platform.git'
176
+
177
+ console.log('Checking prerequisites...\n')
178
+
179
+ const hasBun = checkBun()
180
+ if (!hasBun) {
181
+ console.log('\n❌ Bun is required but not installed.')
182
+ console.log('\nTo install Bun, run:')
183
+ console.log(' curl -fsSL https://bun.sh/install | bash\n')
184
+ console.log('Then run this command again.\n')
185
+ process.exit(1)
186
+ }
187
+
188
+ const hasFirebase = checkFirebaseCLI()
189
+ if (!hasFirebase) {
190
+ console.log('\n❌ Firebase CLI is required but not installed.')
191
+ console.log('\nTo install Firebase CLI, run:')
192
+ console.log(' bun install -g firebase-tools\n')
193
+ console.log('Then run this command again.\n')
194
+ process.exit(1)
195
+ }
196
+
197
+ const isLoggedIn = checkFirebaseLogin()
198
+ if (!isLoggedIn) {
199
+ console.log('\n❌ You need to be logged in to Firebase.')
200
+ console.log('\nRun this command to log in:')
201
+ console.log(' firebase login\n')
202
+ console.log('Then run this command again.\n')
203
+ process.exit(1)
204
+ }
205
+
206
+ console.log('\n📋 Project Configuration\n')
207
+ console.log('Required information:\n')
208
+
209
+ let firebaseProjectId = await question('Firebase Project ID: ')
210
+ while (!validateProjectId(firebaseProjectId)) {
211
+ console.log(
212
+ '❌ Invalid project ID. Use lowercase letters, numbers, and hyphens only (6-30 characters).'
213
+ )
214
+ firebaseProjectId = await question('Firebase Project ID: ')
215
+ }
216
+
217
+ let adminEmail = await question('Admin Email (Google account): ')
218
+ while (!validateEmail(adminEmail)) {
219
+ console.log('❌ Invalid email address.')
220
+ adminEmail = await question('Admin Email (Google account): ')
221
+ }
222
+
223
+ rl.close()
224
+
225
+ const firestoreExists = checkFirestoreExists(firebaseProjectId)
226
+ if (firestoreExists) {
227
+ console.log(
228
+ '\n⚠️ Warning: Firestore database already exists in this project.'
229
+ )
230
+ console.log('This tool will NOT modify your existing data.\n')
231
+ }
232
+
233
+ const hasBlazePlan = checkBlazePlan(firebaseProjectId)
234
+ if (hasBlazePlan === false) {
235
+ console.log('\n🚨 CRITICAL: Blaze Plan Required\n')
236
+ console.log('━'.repeat(60))
237
+ console.log(
238
+ '\ntosijs-platform uses Cloud Functions for secure data access.'
239
+ )
240
+ console.log(
241
+ 'Cloud Functions require the Firebase Blaze (pay-as-you-go) plan.\n'
242
+ )
243
+ console.log('⚠️ Your project is currently on the FREE (Spark) plan.')
244
+ console.log('\n❌ The platform will NOT work without upgrading.\n')
245
+ console.log('To upgrade to Blaze plan:')
246
+ console.log(
247
+ `1. Visit: https://console.firebase.google.com/project/${firebaseProjectId}/usage/details`
248
+ )
249
+ console.log('2. Click "Modify plan"')
250
+ console.log('3. Select "Blaze - Pay as you go"')
251
+ console.log('4. Add a billing account\n')
252
+ console.log('💡 Blaze plan includes generous free tier:')
253
+ console.log(' • 2M function invocations/month FREE')
254
+ console.log(' • 5GB storage FREE')
255
+ console.log(' • Most small sites stay within free limits\n')
256
+ console.log('━'.repeat(60))
257
+
258
+ const answer = await new Promise((resolve) => {
259
+ const newRl = readline.createInterface({
260
+ input: process.stdin,
261
+ output: process.stdout,
262
+ })
263
+ newRl.question(
264
+ '\nContinue anyway? (you must upgrade before deploying) [y/N]: ',
265
+ (ans) => {
266
+ newRl.close()
267
+ resolve(ans)
268
+ }
269
+ )
270
+ })
271
+
272
+ if (!answer || answer.toLowerCase() !== 'y') {
273
+ console.log(
274
+ '\nSetup cancelled. Please upgrade to Blaze plan and try again.\n'
275
+ )
276
+ process.exit(0)
277
+ }
278
+
279
+ console.log(
280
+ '\n⚠️ Remember: You MUST upgrade to Blaze plan before deploying!\n'
281
+ )
282
+ } else if (hasBlazePlan === true) {
283
+ console.log('✓ Blaze plan detected - Cloud Functions enabled')
284
+ } else {
285
+ console.log('⚠️ Could not verify billing plan (you may need Blaze plan)')
286
+ }
287
+
288
+ const firebaseConfig = getFirebaseConfig(firebaseProjectId)
289
+
290
+ if (!firebaseConfig) {
291
+ console.log('\n⚠️ Could not automatically fetch Firebase config.')
292
+ console.log('\nYou will need to manually configure src/firebase-config.ts')
293
+ console.log('Get your config from:')
294
+ console.log(
295
+ `https://console.firebase.google.com/project/${firebaseProjectId}/settings/general\n`
296
+ )
297
+ }
298
+
299
+ console.log('\n📦 Creating project...\n')
300
+
301
+ try {
302
+ fs.mkdirSync(projectPath)
303
+ } catch (err) {
304
+ if (err.code === 'EEXIST') {
305
+ console.log(`❌ Directory "${projectName}" already exists.\n`)
306
+ } else {
307
+ console.log(`❌ Error creating directory: ${err.message}\n`)
308
+ }
309
+ process.exit(1)
310
+ }
311
+
312
+ console.log('📥 Downloading template...')
313
+ execCommand(`git clone --depth 1 ${gitRepo} ${projectPath}`)
314
+
315
+ process.chdir(projectPath)
316
+
317
+ console.log('🧹 Cleaning up...')
318
+ fs.rmSync(path.join(projectPath, '.git'), { recursive: true })
319
+ if (fs.existsSync(path.join(projectPath, 'bin'))) {
320
+ fs.rmSync(path.join(projectPath, 'bin'), { recursive: true })
321
+ }
322
+
323
+ // Configure initial_state: update owner role with admin email
324
+ // (keeps other test roles for emulator development)
325
+ console.log('📝 Configuring initial state...')
326
+ const roleFilePath = path.join(
327
+ projectPath,
328
+ 'initial_state/firestore/role.json'
329
+ )
330
+ if (fs.existsSync(roleFilePath)) {
331
+ const roles = JSON.parse(fs.readFileSync(roleFilePath, 'utf8'))
332
+ const ownerRole = roles.find((r) => r._id === 'owner-role')
333
+ if (ownerRole) {
334
+ // Update the owner role's email contact
335
+ ownerRole.contacts = [{ type: 'email', value: adminEmail }]
336
+ }
337
+ fs.writeFileSync(roleFilePath, JSON.stringify(roles, null, 2))
338
+ }
339
+
340
+ // Remove test auth users (only used for emulator development)
341
+ const authUsersPath = path.join(projectPath, 'initial_state/auth/users.json')
342
+ if (fs.existsSync(authUsersPath)) {
343
+ fs.writeFileSync(authUsersPath, '[]')
344
+ }
345
+
346
+ console.log('⚙️ Configuring project...')
347
+
348
+ let configContent
349
+ if (firebaseConfig) {
350
+ configContent = `const PROJECT_ID = '${firebaseProjectId}'
351
+
352
+ export const PRODUCTION_BASE = \`//us-central1-\${PROJECT_ID}.cloudfunctions.net/\`
353
+
354
+ export const config = {
355
+ authDomain: '${firebaseConfig.authDomain}',
356
+ projectId: '${firebaseConfig.projectId}',
357
+ storageBucket: '${firebaseConfig.storageBucket}',
358
+ apiKey: '${firebaseConfig.apiKey}',
359
+ messagingSenderId: '${firebaseConfig.messagingSenderId}',
360
+ appId: '${firebaseConfig.appId}',
361
+ measurementId: '${firebaseConfig.measurementId || 'G-XXXXXXXXXX'}',
362
+ }
363
+ `
364
+ } else {
365
+ configContent = `const PROJECT_ID = '${firebaseProjectId}'
366
+
367
+ export const PRODUCTION_BASE = \`//us-central1-\${PROJECT_ID}.cloudfunctions.net/\`
368
+
369
+ export const config = {
370
+ authDomain: \`\${PROJECT_ID}.firebaseapp.com\`,
371
+ projectId: PROJECT_ID,
372
+ storageBucket: \`\${PROJECT_ID}.appspot.com\`,
373
+ apiKey: 'YOUR_API_KEY_HERE',
374
+ messagingSenderId: 'YOUR_SENDER_ID_HERE',
375
+ appId: 'YOUR_APP_ID_HERE',
376
+ measurementId: 'YOUR_MEASUREMENT_ID_HERE',
377
+ }
378
+ `
379
+ }
380
+
381
+ fs.writeFileSync(
382
+ path.join(projectPath, 'src/firebase-config.ts'),
383
+ configContent
384
+ )
385
+
386
+ // Update config.json with project-specific values
387
+ const configJsonPath = path.join(
388
+ projectPath,
389
+ 'initial_state/firestore/config.json'
390
+ )
391
+ const configData = JSON.parse(fs.readFileSync(configJsonPath, 'utf8'))
392
+
393
+ // Update app config
394
+ const appConfig = configData.find((c) => c._id === 'app')
395
+ if (appConfig) {
396
+ appConfig.title = projectName
397
+ appConfig.subtitle = `Welcome to ${projectName}`
398
+ appConfig.description = `A tosijs-platform site`
399
+ appConfig.host = `${firebaseProjectId}.web.app`
400
+ }
401
+
402
+ // Update blog config
403
+ const blogConfig = configData.find((c) => c._id === 'blog')
404
+ if (blogConfig) {
405
+ blogConfig.prefix = `${projectName} | `
406
+ }
407
+
408
+ fs.writeFileSync(configJsonPath, JSON.stringify(configData, null, 2))
409
+
410
+ const firebaserc = {
411
+ projects: {
412
+ default: firebaseProjectId,
413
+ },
414
+ }
415
+
416
+ fs.writeFileSync(
417
+ path.join(projectPath, '.firebaserc'),
418
+ JSON.stringify(firebaserc, null, 2)
419
+ )
420
+
421
+ const indexHtml = `<!DOCTYPE html>
422
+ <html lang="en">
423
+ <head>
424
+ <meta charset="utf-8" />
425
+
426
+ <title>${projectName}</title>
427
+ <meta name="description" content="A tosijs-platform site" />
428
+ <meta property="og:url" content="" />
429
+ <meta property="og:title" content="" />
430
+ <meta property="og:description" content="" />
431
+ <meta property="og:image" content="/logo.png" />
432
+
433
+ <link rel="icon" href="/favicon.ico" />
434
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
435
+ <meta name="theme-color" content="#000000" />
436
+ <link rel="apple-touch-icon" href="/logo.png" />
437
+ <link rel="manifest" href="/manifest.json" />
438
+ <script type="module" src="/index.js"></script>
439
+ </head>
440
+ <body></body>
441
+ </html>
442
+ `
443
+
444
+ fs.writeFileSync(path.join(projectPath, 'public/index.html'), indexHtml)
445
+
446
+ const packageJson = JSON.parse(
447
+ fs.readFileSync(path.join(projectPath, 'package.json'), 'utf8')
448
+ )
449
+ packageJson.name = projectName
450
+ packageJson.version = '0.1.0'
451
+ delete packageJson.bin
452
+
453
+ fs.writeFileSync(
454
+ path.join(projectPath, 'package.json'),
455
+ JSON.stringify(packageJson, null, 2)
456
+ )
457
+
458
+ const setupScript = `#!/usr/bin/env node
459
+
460
+ /*
461
+ * Setup admin user and seed initial content
462
+ * Run after deploying functions: node setup.js
463
+ */
464
+
465
+ import admin from 'firebase-admin'
466
+ import fs from 'fs'
467
+ import path from 'path'
468
+ import { fileURLToPath } from 'url'
469
+
470
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
471
+
472
+ const ADMIN_EMAIL = '${adminEmail}'
473
+ const PROJECT_ID = '${firebaseProjectId}'
474
+ const SKIP_EXISTING_DATA = ${firestoreExists}
475
+
476
+ admin.initializeApp({
477
+ projectId: PROJECT_ID,
478
+ })
479
+
480
+ const db = admin.firestore()
481
+
482
+ async function seedFirestore() {
483
+ if (SKIP_EXISTING_DATA) {
484
+ console.log('Skipping seed data: database already exists')
485
+ return
486
+ }
487
+
488
+ console.log('Seeding Firestore from initial_state...')
489
+ const firestorePath = path.join(__dirname, 'initial_state/firestore')
490
+
491
+ if (!fs.existsSync(firestorePath)) {
492
+ console.log(' No seed data found')
493
+ return
494
+ }
495
+
496
+ const files = fs.readdirSync(firestorePath).filter(f => f.endsWith('.json'))
497
+
498
+ for (const file of files) {
499
+ const collectionPath = file.replace(/\\.json$/, '').replace(/\\|/g, '/')
500
+ const filePath = path.join(firestorePath, file)
501
+
502
+ try {
503
+ const documents = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
504
+
505
+ if (!Array.isArray(documents)) continue
506
+
507
+ let count = 0
508
+ for (const doc of documents) {
509
+ const docId = doc._id
510
+ if (!docId) continue
511
+
512
+ const data = { ...doc }
513
+ delete data._id
514
+
515
+ const now = new Date().toISOString()
516
+ data._created = data._created || now
517
+ data._modified = data._modified || now
518
+ data._path = \`\${collectionPath}/\${docId}\`
519
+
520
+ await db.collection(collectionPath).doc(docId).set(data)
521
+ count++
522
+ }
523
+
524
+ console.log(\` \${collectionPath}: \${count} documents\`)
525
+ } catch (error) {
526
+ console.log(\` Error processing \${file}: \${error.message}\`)
527
+ }
528
+ }
529
+ }
530
+
531
+ async function setupAdminRole() {
532
+ console.log('Setting up admin role...\\n')
533
+
534
+ try {
535
+ const userByEmail = await admin.auth().getUserByEmail(ADMIN_EMAIL)
536
+ const userId = userByEmail.uid
537
+
538
+ console.log(\`Found user: \${ADMIN_EMAIL} (uid: \${userId})\`)
539
+
540
+ // Update the owner role document to include this user's UID
541
+ const rolesSnapshot = await db.collection('role')
542
+ .where('contacts', 'array-contains', { type: 'email', value: ADMIN_EMAIL })
543
+ .get()
544
+
545
+ if (!rolesSnapshot.empty) {
546
+ for (const doc of rolesSnapshot.docs) {
547
+ const data = doc.data()
548
+ if (!data.userIds?.includes(userId)) {
549
+ await doc.ref.update({
550
+ userIds: admin.firestore.FieldValue.arrayUnion(userId)
551
+ })
552
+ console.log(\`Added uid to role: \${data.name}\`)
553
+ } else {
554
+ console.log(\`User already in role: \${data.name}\`)
555
+ }
556
+ }
557
+ } else {
558
+ console.log('No matching role found, creating owner role...')
559
+ await db.collection('role').doc('owner-role').set({
560
+ name: 'Owner',
561
+ contacts: [{ type: 'email', value: ADMIN_EMAIL }],
562
+ roles: ['owner', 'developer', 'admin', 'editor', 'author'],
563
+ userIds: [userId],
564
+ _created: new Date().toISOString(),
565
+ _modified: new Date().toISOString(),
566
+ _path: 'role/owner-role'
567
+ })
568
+ }
569
+
570
+ console.log('\\nAdmin role setup complete!')
571
+ } catch (error) {
572
+ if (error.code === 'auth/user-not-found') {
573
+ console.error(\`\\nUser \${ADMIN_EMAIL} does not exist in Firebase Auth.\`)
574
+ console.error('\\nNext steps:')
575
+ console.error(' 1. Deploy functions: bun deploy-functions')
576
+ console.error(' 2. Deploy hosting: bun deploy-hosting')
577
+ console.error(\` 3. Visit your site and sign in with \${ADMIN_EMAIL}\`)
578
+ console.error(' 4. Run this script again: node setup.js')
579
+ return false
580
+ }
581
+ throw error
582
+ }
583
+ return true
584
+ }
585
+
586
+ async function setup() {
587
+ console.log('\\nSetting up ${projectName}...\\n')
588
+
589
+ try {
590
+ await seedFirestore()
591
+ console.log('')
592
+ const success = await setupAdminRole()
593
+
594
+ if (success) {
595
+ console.log(\`\\nYou can now:
596
+ 1. Run: bun start
597
+ 2. Visit: https://localhost:8020
598
+ 3. Sign in with: ${adminEmail}
599
+ \`)
600
+ }
601
+ } catch (error) {
602
+ console.error('Setup failed:', error.message)
603
+ console.error('\\nMake sure:')
604
+ console.error(' 1. Functions are deployed: bun deploy-functions')
605
+ console.error(' 2. Firebase services are enabled (Auth, Firestore, Storage)')
606
+ process.exit(1)
607
+ }
608
+ }
609
+
610
+ setup()
611
+ `
612
+
613
+ fs.writeFileSync(path.join(projectPath, 'setup.js'), setupScript)
614
+ fs.chmodSync(path.join(projectPath, 'setup.js'), '755')
615
+
616
+ console.log('📦 Installing client dependencies...')
617
+ execCommand('bun install')
618
+
619
+ console.log('📦 Installing function dependencies...')
620
+ execCommand('cd functions && bun install && cd ..')
621
+
622
+ console.log('🔐 Generating TLS certificates for local development...')
623
+ execCommand('cd tls && ./create-dev-certs.sh && cd ..', { silent: true })
624
+
625
+ console.log('\n✅ Project created successfully!\n')
626
+ console.log('━'.repeat(60))
627
+ console.log(`\n📁 Project: ${projectName}`)
628
+ console.log(`🔥 Firebase: ${firebaseProjectId}`)
629
+ console.log(`👤 Admin: ${adminEmail}`)
630
+ if (firestoreExists) {
631
+ console.log(`⚠️ Existing data: Will be preserved`)
632
+ }
633
+ console.log('\n━'.repeat(60))
634
+
635
+ console.log('\n📋 Next Steps:\n')
636
+
637
+ if (hasBlazePlan === false) {
638
+ console.log('⚠️ FIRST: Upgrade to Blaze Plan (REQUIRED)')
639
+ console.log(
640
+ ` https://console.firebase.google.com/project/${firebaseProjectId}/usage/details`
641
+ )
642
+ console.log(' Without Blaze plan, Cloud Functions will not work!\n')
643
+ }
644
+
645
+ if (!firebaseConfig) {
646
+ console.log(
647
+ `${hasBlazePlan === false ? '1' : '1'}. Get your Firebase Web App config:`
648
+ )
649
+ console.log(
650
+ ` https://console.firebase.google.com/project/${firebaseProjectId}/settings/general`
651
+ )
652
+ console.log(` • Scroll to "Your apps" → Add web app (if none exist)`)
653
+ console.log(` • Copy the config object`)
654
+ console.log(` • Update src/firebase-config.ts with the values\n`)
655
+ } else {
656
+ console.log(
657
+ `${
658
+ hasBlazePlan === false ? '1' : '1'
659
+ }. ✓ Firebase config automatically configured\n`
660
+ )
661
+ }
662
+
663
+ console.log(`2. Enable Firebase services (if not already enabled):`)
664
+ console.log(
665
+ ` https://console.firebase.google.com/project/${firebaseProjectId}`
666
+ )
667
+ console.log(` • Authentication → Google sign-in method`)
668
+ console.log(` • Firestore Database → Create database (production mode)`)
669
+ console.log(` • Cloud Storage → Get started`)
670
+ if (hasBlazePlan !== true) {
671
+ console.log(` • ⚠️ UPGRADE TO BLAZE PLAN (required for Cloud Functions)`)
672
+ }
673
+
674
+ console.log(`\n3. Deploy Cloud Functions:`)
675
+ console.log(` cd ${projectName}`)
676
+ console.log(` bun deploy-functions`)
677
+
678
+ console.log(`\n4. Deploy Hosting:`)
679
+ console.log(` bun deploy-hosting`)
680
+
681
+ console.log(`\n5. Visit your site and sign in with ${adminEmail}`)
682
+ console.log(` https://${firebaseProjectId}.web.app`)
683
+
684
+ console.log(`\n6. Run setup to grant admin access:`)
685
+ console.log(` node setup.js`)
686
+
687
+ console.log(`\n7. Start local development:`)
688
+ console.log(` bun start`)
689
+ console.log(` Visit https://localhost:8020`)
690
+
691
+ console.log('\n━'.repeat(60))
692
+ console.log('\n📚 Custom Domain Setup:\n')
693
+ console.log('To add a custom domain (e.g., yoursite.com):')
694
+ console.log(
695
+ `1. Go to: https://console.firebase.google.com/project/${firebaseProjectId}/hosting/sites`
696
+ )
697
+ console.log('2. Click "Add custom domain"')
698
+ console.log('3. Follow the wizard to:')
699
+ console.log(' • Verify domain ownership (add TXT record to DNS)')
700
+ console.log(' • Add A/AAAA records to point to Firebase')
701
+ console.log('4. Wait for SSL certificate provisioning (~15 minutes)')
702
+ console.log(
703
+ '5. Update the host field in the config/app document in Firestore:'
704
+ )
705
+ console.log(
706
+ ` https://console.firebase.google.com/project/${firebaseProjectId}/firestore/data/~2Fconfig~2Fapp`
707
+ )
708
+ console.log(` Set host to: yoursite.com\n`)
709
+
710
+ console.log('━'.repeat(60))
711
+ console.log('\n🔐 API Keys for AI Features (Optional):\n')
712
+ console.log('If you want to use the /gen endpoint for AI completions,')
713
+ console.log('you need to set up API keys using Firebase Secrets Manager:\n')
714
+ console.log(' # For Gemini (Google AI):')
715
+ console.log(
716
+ ' firebase functions:secrets:set gemini-api-key --project ' +
717
+ firebaseProjectId
718
+ )
719
+ console.log(' # Get your key at: https://aistudio.google.com/apikey\n')
720
+ console.log(' # For ChatGPT (OpenAI):')
721
+ console.log(
722
+ ' firebase functions:secrets:set chatgpt-api-key --project ' +
723
+ firebaseProjectId
724
+ )
725
+ console.log(' # Get your key at: https://platform.openai.com/api-keys\n')
726
+ console.log(
727
+ 'After setting secrets, redeploy functions: bun deploy-functions\n'
728
+ )
729
+
730
+ console.log('━'.repeat(60))
731
+ console.log(
732
+ '\n📖 Documentation: https://github.com/tonioloewald/tosijs-platform'
733
+ )
734
+ console.log(
735
+ '💬 Issues: https://github.com/tonioloewald/tosijs-platform/issues'
736
+ )
737
+ console.log('\n🎉 Happy building!\n')
738
+ }
739
+
740
+ main().catch((error) => {
741
+ console.error('\n❌ Error:', error.message)
742
+ process.exit(1)
743
+ })
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "create-tosijs-platform-app",
3
+ "version": "1.0.0",
4
+ "description": "Create a full-stack web app with tosijs (front-end) and Firebase (back-end)",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-tosijs-platform-app": "./create-tosijs-platform-app.js"
8
+ },
9
+ "files": [
10
+ "create-tosijs-platform-app.js"
11
+ ],
12
+ "keywords": [
13
+ "tosijs",
14
+ "firebase",
15
+ "create",
16
+ "scaffold",
17
+ "fullstack",
18
+ "web-app"
19
+ ],
20
+ "author": "Tonio Loewald",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/tonioloewald/tosijs-platform.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/tonioloewald/tosijs-platform/issues"
28
+ },
29
+ "homepage": "https://github.com/tonioloewald/tosijs-platform#readme",
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ }
33
+ }