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 +73 -0
- package/create-tosijs-platform-app.js +743 -0
- package/package.json +33 -0
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
|
+
}
|