create-tosijs-platform-app 1.0.3 → 1.0.5

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