create-snappy 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 +35 -0
- package/package.json +21 -0
- package/scripts/create-snappy/cli.js +432 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @snappy-stack/cli
|
|
2
|
+
|
|
3
|
+
The official CLI installer to bootstrap the SNAPPY Stack.
|
|
4
|
+
|
|
5
|
+
The SNAPPY Stack is a high-performance, opinionated architecture built exclusively on **Next.js 16**, **Payload CMS 3.0**, and **Tailwind CSS**.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
You can create a new SNAPPY project interactively by running:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx create-snappy my-snappy-app
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or just run the CLI without a directory name to trigger the guided wizard:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx create-snappy
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- **Guided Infrastructure Setup**: Automatically configs your `.env` for Supabase Postgres and S3 Storage.
|
|
24
|
+
- **Private Templates**: Secured GitHub OAuth flow for accessing proprietary premium templates.
|
|
25
|
+
- **Lightning Fast**: Shallow git cloning for instant scaffolding.
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- `Node.js` >= 20.x
|
|
30
|
+
- `pnpm` >= 9.x
|
|
31
|
+
- Git
|
|
32
|
+
|
|
33
|
+
## License Requirement
|
|
34
|
+
|
|
35
|
+
The underlying engine (`@snappy-stack/core`) requires a valid SNAPPY Development License to function.
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-snappy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The official installer for the SNAPPY stack.",
|
|
5
|
+
"main": "scripts/create-snappy/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-snappy": "./scripts/create-snappy/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"commander": "^14.0.3",
|
|
15
|
+
"picocolors": "^1.1.1",
|
|
16
|
+
"prompts": "^2.4.2"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* create-snappy
|
|
5
|
+
* The official CLI to bootstrap the SNAPPY stack.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs'
|
|
9
|
+
import path from 'path'
|
|
10
|
+
import { fileURLToPath } from 'url'
|
|
11
|
+
import { execSync } from 'child_process'
|
|
12
|
+
import readline from 'readline'
|
|
13
|
+
import os from 'os'
|
|
14
|
+
import { Command } from 'commander'
|
|
15
|
+
import prompts from 'prompts'
|
|
16
|
+
import pc from 'picocolors'
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
19
|
+
const __dirname = path.dirname(__filename)
|
|
20
|
+
|
|
21
|
+
const CONFIG_DIR = path.join(os.homedir(), '.snappy')
|
|
22
|
+
const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json')
|
|
23
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
24
|
+
|
|
25
|
+
// This is the Client ID for GitHub Device OAuth
|
|
26
|
+
const CLIENT_ID = 'Ov23liWHoGiMponaUxrc'
|
|
27
|
+
|
|
28
|
+
const rl = readline.createInterface({
|
|
29
|
+
input: process.stdin,
|
|
30
|
+
output: process.stdout,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const question = (query) => new Promise((resolve) => rl.question(query, resolve))
|
|
34
|
+
|
|
35
|
+
// --- Auth & Config Utilities ---
|
|
36
|
+
|
|
37
|
+
function getSavedToken() {
|
|
38
|
+
if (fs.existsSync(AUTH_FILE)) {
|
|
39
|
+
try {
|
|
40
|
+
const data = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf8'))
|
|
41
|
+
return data.access_token
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function saveToken(token) {
|
|
50
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
51
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
52
|
+
}
|
|
53
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify({ access_token: token }, null, 2))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getSavedConfig() {
|
|
57
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'))
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return {}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function saveConfig(config) {
|
|
68
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
69
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
70
|
+
}
|
|
71
|
+
const current = getSavedConfig()
|
|
72
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...config }, null, 2))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function githubLogin() {
|
|
76
|
+
console.log('\nš Authenticating with GitHub...')
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch('https://github.com/login/device/code', {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
82
|
+
body: JSON.stringify({ client_id: CLIENT_ID, scope: 'repo read:user' }),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (response.status === 404) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
'GitHub returned 404. This usually means the Client ID is invalid or the app is not registered.',
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = await response.json()
|
|
92
|
+
if (data.error) throw new Error(`Auth request failed: ${data.error_description || data.error}`)
|
|
93
|
+
|
|
94
|
+
const { device_code, user_code, verification_uri, interval, expires_in } = data
|
|
95
|
+
|
|
96
|
+
console.log(`\n1. Go to: \x1b[34m${verification_uri}\x1b[0m`)
|
|
97
|
+
console.log(`2. Enter code: \x1b[1m\x1b[32m${user_code}\x1b[0m`)
|
|
98
|
+
console.log(
|
|
99
|
+
`\nWaiting for authorization... (Expires in ${Math.floor(expires_in / 60)} minutes)`,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const poll = async () => {
|
|
103
|
+
const res = await fetch('https://github.com/login/oauth/access_token', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
client_id: CLIENT_ID,
|
|
108
|
+
device_code,
|
|
109
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
110
|
+
}),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const tokenData = await res.json()
|
|
114
|
+
|
|
115
|
+
if (tokenData.error) {
|
|
116
|
+
if (tokenData.error === 'authorization_pending') {
|
|
117
|
+
await new Promise((r) => setTimeout(r, (interval || 5) * 1000))
|
|
118
|
+
return poll()
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`Polling failed: ${tokenData.error_description || tokenData.error}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return tokenData.access_token
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const accessToken = await poll()
|
|
127
|
+
saveToken(accessToken)
|
|
128
|
+
console.log('ā
Successfully authenticated!')
|
|
129
|
+
return accessToken
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn(`\x1b[33m\nā ļø GitHub OAuth failed: ${err.message}\x1b[0m`)
|
|
132
|
+
console.log('\x1b[36mFalling back to manual token entry...\x1b[0m')
|
|
133
|
+
const manualToken = await question('Paste your GitHub Personal Access Token (PAT): ')
|
|
134
|
+
|
|
135
|
+
if (!manualToken || manualToken.trim() === '') {
|
|
136
|
+
throw new Error('No token provided. Installation aborted.')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
saveToken(manualToken.trim())
|
|
140
|
+
console.log('ā
Token saved manually.')
|
|
141
|
+
return manualToken.trim()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Main CLI ---
|
|
146
|
+
|
|
147
|
+
async function main() {
|
|
148
|
+
console.log(pc.cyan('\nš Welcome to the SNAPPY Stack Installer!'))
|
|
149
|
+
console.log('------------------------------------------')
|
|
150
|
+
|
|
151
|
+
const program = new Command()
|
|
152
|
+
program
|
|
153
|
+
.name('create-snappy')
|
|
154
|
+
.description('The official installer for the SNAPPY stack. (Private Access Required)')
|
|
155
|
+
.argument('[directory]', 'Project directory name')
|
|
156
|
+
.option('-t, --template <name>', 'Template to use (main, porto)')
|
|
157
|
+
.option('--login', 'Authenticate with GitHub to access private templates')
|
|
158
|
+
.option('--guided', 'Force guided setup for environment variables')
|
|
159
|
+
.parse(process.argv)
|
|
160
|
+
|
|
161
|
+
const options = program.opts()
|
|
162
|
+
let providedName = program.args[0]
|
|
163
|
+
|
|
164
|
+
if (options.login) {
|
|
165
|
+
try {
|
|
166
|
+
await githubLogin()
|
|
167
|
+
rl.close()
|
|
168
|
+
process.exit(0)
|
|
169
|
+
} catch (e) {
|
|
170
|
+
console.error(pc.red(e.message))
|
|
171
|
+
rl.close()
|
|
172
|
+
process.exit(1)
|
|
173
|
+
}
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const savedConfig = getSavedConfig()
|
|
178
|
+
const hasSavedConfig = Object.keys(savedConfig).length > 0
|
|
179
|
+
|
|
180
|
+
// INTERACTIVE PROMPTS
|
|
181
|
+
const questions = []
|
|
182
|
+
|
|
183
|
+
if (!providedName) {
|
|
184
|
+
questions.push({
|
|
185
|
+
type: 'text',
|
|
186
|
+
name: 'projectName',
|
|
187
|
+
message: 'What is your project named?',
|
|
188
|
+
initial: 'my-snappy-app',
|
|
189
|
+
validate: (value) => (value.length > 0 ? true : 'Please enter a project name.'),
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
questions.push({
|
|
194
|
+
type: 'text',
|
|
195
|
+
name: 'projectDescription',
|
|
196
|
+
message: 'Project description?',
|
|
197
|
+
initial: 'A fresh SNAPPY app',
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
questions.push({
|
|
201
|
+
type: 'text',
|
|
202
|
+
name: 'authorName',
|
|
203
|
+
message: 'Author name?',
|
|
204
|
+
initial: savedConfig.authorName || 'Wicky',
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
if (!options.template) {
|
|
208
|
+
questions.push({
|
|
209
|
+
type: 'select',
|
|
210
|
+
name: 'template',
|
|
211
|
+
message: 'Which template would you like to use?',
|
|
212
|
+
choices: [
|
|
213
|
+
{ title: 'Portfolio', description: 'A sleek portfolio template', value: 'portofolio' },
|
|
214
|
+
],
|
|
215
|
+
initial: 0,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Guided Setup Questions
|
|
220
|
+
const needsGuided = options.guided || !hasSavedConfig
|
|
221
|
+
|
|
222
|
+
if (needsGuided) {
|
|
223
|
+
console.log(pc.yellow('\nš ļø Guided Setup: Please enter your infrastructure details.'))
|
|
224
|
+
console.log(pc.gray('These will be saved to ~/.snappy/config.json for future use.\n'))
|
|
225
|
+
|
|
226
|
+
questions.push(
|
|
227
|
+
{
|
|
228
|
+
type: 'text',
|
|
229
|
+
name: 'supabaseUrl',
|
|
230
|
+
message: 'Supabase URL?',
|
|
231
|
+
initial: savedConfig.supabaseUrl || '',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
type: 'password',
|
|
235
|
+
name: 'supabaseAnonKey',
|
|
236
|
+
message: 'Supabase Anon Key?',
|
|
237
|
+
initial: savedConfig.supabaseAnonKey || '',
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
type: 'password',
|
|
241
|
+
name: 'supabaseServiceRole',
|
|
242
|
+
message: 'Supabase Service Role Key?',
|
|
243
|
+
initial: savedConfig.supabaseServiceRole || '',
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
type: 'text',
|
|
247
|
+
name: 's3Bucket',
|
|
248
|
+
message: 'S3 Bucket Name?',
|
|
249
|
+
initial: savedConfig.s3Bucket || 'snappy-storage',
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
type: 'text',
|
|
253
|
+
name: 's3Region',
|
|
254
|
+
message: 'S3 Region?',
|
|
255
|
+
initial: savedConfig.s3Region || 'ap-southeast-1',
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
type: 'text',
|
|
259
|
+
name: 's3Endpoint',
|
|
260
|
+
message: 'S3 Endpoint URL?',
|
|
261
|
+
initial: savedConfig.s3Endpoint || '',
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
type: 'password',
|
|
265
|
+
name: 's3AccessKey',
|
|
266
|
+
message: 'S3 Access Key ID?',
|
|
267
|
+
initial: savedConfig.s3AccessKey || '',
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
type: 'password',
|
|
271
|
+
name: 's3SecretKey',
|
|
272
|
+
message: 'S3 Secret Access Key?',
|
|
273
|
+
initial: savedConfig.s3SecretKey || '',
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const response = await prompts(questions, {
|
|
279
|
+
onCancel: () => {
|
|
280
|
+
console.log(pc.yellow('Installation cancelled.'))
|
|
281
|
+
process.exit(0)
|
|
282
|
+
},
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// Merge responses with saved config
|
|
286
|
+
const config = {
|
|
287
|
+
...savedConfig,
|
|
288
|
+
authorName: response.authorName || savedConfig.authorName,
|
|
289
|
+
supabaseUrl: response.supabaseUrl || savedConfig.supabaseUrl,
|
|
290
|
+
supabaseAnonKey: response.supabaseAnonKey || savedConfig.supabaseAnonKey,
|
|
291
|
+
supabaseServiceRole: response.supabaseServiceRole || savedConfig.supabaseServiceRole,
|
|
292
|
+
s3Bucket: response.s3Bucket || savedConfig.s3Bucket,
|
|
293
|
+
s3Region: response.s3Region || savedConfig.s3Region,
|
|
294
|
+
s3Endpoint: response.s3Endpoint || savedConfig.s3Endpoint,
|
|
295
|
+
s3AccessKey: response.s3AccessKey || savedConfig.s3AccessKey,
|
|
296
|
+
s3SecretKey: response.s3SecretKey || savedConfig.s3SecretKey,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Save if it was a guided session or forced
|
|
300
|
+
if (needsGuided) {
|
|
301
|
+
saveConfig(config)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const projectName = providedName || response.projectName
|
|
305
|
+
const projectDescription = response.projectDescription
|
|
306
|
+
const authorName = config.authorName
|
|
307
|
+
const selectedTemplate = options.template || response.template || 'portofolio'
|
|
308
|
+
|
|
309
|
+
const targetDir = path.resolve(process.cwd(), projectName)
|
|
310
|
+
|
|
311
|
+
if (fs.existsSync(targetDir)) {
|
|
312
|
+
const { overwrite } = await prompts({
|
|
313
|
+
type: 'confirm',
|
|
314
|
+
name: 'overwrite',
|
|
315
|
+
message: pc.yellow(`Directory ${projectName} already exists. Overwrite?`),
|
|
316
|
+
initial: false,
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
if (!overwrite) {
|
|
320
|
+
console.log('Aborted.')
|
|
321
|
+
process.exit(0)
|
|
322
|
+
}
|
|
323
|
+
fs.rmSync(targetDir, { recursive: true, force: true })
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Create target dir
|
|
327
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
// 1. Initialize from repository
|
|
331
|
+
const token = getSavedToken() || (await githubLogin())
|
|
332
|
+
|
|
333
|
+
const repoUrl = process.env.SNAPPY_REPO_URL || 'https://github.com/snappy-stack/snappy'
|
|
334
|
+
let authenticatedUrl = repoUrl
|
|
335
|
+
|
|
336
|
+
if (token) {
|
|
337
|
+
authenticatedUrl = repoUrl.replace('https://', `https://x-access-token:${token}@`)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
console.log(`Cloning into ${pc.bold(projectName)}...`)
|
|
341
|
+
try {
|
|
342
|
+
// Use branch as the template name
|
|
343
|
+
execSync(`git clone --depth 1 -b ${selectedTemplate} ${authenticatedUrl} "${targetDir}"`, {
|
|
344
|
+
stdio: 'inherit',
|
|
345
|
+
})
|
|
346
|
+
} catch (cloneErr) {
|
|
347
|
+
if (!token) {
|
|
348
|
+
console.log(pc.yellow('\nš This template might be private. Attempting login...'))
|
|
349
|
+
token = await githubLogin()
|
|
350
|
+
authenticatedUrl = repoUrl.replace('https://', `https://x-access-token:${token}@`)
|
|
351
|
+
execSync(`git clone --depth 1 -b ${selectedTemplate} ${authenticatedUrl} "${targetDir}"`, {
|
|
352
|
+
stdio: 'inherit',
|
|
353
|
+
})
|
|
354
|
+
} else {
|
|
355
|
+
console.error(
|
|
356
|
+
pc.red("Clone failed. The branch might not exist, or you don't have access."),
|
|
357
|
+
)
|
|
358
|
+
throw cloneErr
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (fs.existsSync(path.join(targetDir, '.git'))) {
|
|
363
|
+
fs.rmSync(path.join(targetDir, '.git'), { recursive: true, force: true })
|
|
364
|
+
}
|
|
365
|
+
console.log(pc.green('ā
Project initialized from secure source.'))
|
|
366
|
+
|
|
367
|
+
// 2. Customize package.json and README.md
|
|
368
|
+
console.log('\nš Customizing project files...')
|
|
369
|
+
const pkgPath = path.join(targetDir, 'package.json')
|
|
370
|
+
if (fs.existsSync(pkgPath)) {
|
|
371
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
372
|
+
pkg.name = projectName.toLowerCase().replace(/\s+/g, '-')
|
|
373
|
+
pkg.description = projectDescription || pkg.description
|
|
374
|
+
pkg.author = authorName || pkg.author
|
|
375
|
+
if (pkg.bin) delete pkg.bin
|
|
376
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2))
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const readmePath = path.join(targetDir, 'README.md')
|
|
380
|
+
if (fs.existsSync(readmePath)) {
|
|
381
|
+
const readmeContent = `# ${projectName}\n\n${projectDescription}\n\nGenerated with \`create-snappy\`.`
|
|
382
|
+
fs.writeFileSync(readmePath, readmeContent)
|
|
383
|
+
}
|
|
384
|
+
console.log(pc.green('ā
Project details injected.'))
|
|
385
|
+
|
|
386
|
+
// 3. Configure .env
|
|
387
|
+
console.log('\nš ļø Configuring environment variables...')
|
|
388
|
+
const envContent = `NEXT_PUBLIC_SUPABASE_URL="${config.supabaseUrl}"
|
|
389
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY="${config.supabaseAnonKey}"
|
|
390
|
+
SUPABASE_SERVICE_ROLE_KEY="${config.supabaseServiceRole}"
|
|
391
|
+
SUPABASE_URL="${config.supabaseUrl}"
|
|
392
|
+
|
|
393
|
+
# Database Configuration
|
|
394
|
+
POSTGRES_URL="postgres://postgres:${config.supabaseServiceRole}@${config.supabaseUrl.replace('https://', '').split('.')[0]}.supabase.co:5432/postgres"
|
|
395
|
+
|
|
396
|
+
# S3 Storage Configuration
|
|
397
|
+
S3_BUCKET="${config.s3Bucket}"
|
|
398
|
+
S3_REGION="${config.s3Region}"
|
|
399
|
+
S3_ENDPOINT="${config.s3Endpoint}"
|
|
400
|
+
S3_ACCESS_KEY_ID="${config.s3AccessKey}"
|
|
401
|
+
S3_SECRET_ACCESS_KEY="${config.s3SecretKey}"
|
|
402
|
+
|
|
403
|
+
PAYLOAD_SECRET="${Math.random().toString(36).substring(2)}"
|
|
404
|
+
PUBLIC_FRONTEND_URL="http://localhost:3000"
|
|
405
|
+
REQUIRE_LOGIN="yes"
|
|
406
|
+
`
|
|
407
|
+
fs.writeFileSync(path.join(targetDir, '.env'), envContent)
|
|
408
|
+
console.log(pc.green('ā
.env generated.'))
|
|
409
|
+
|
|
410
|
+
if (process.env.SKIP_INSTALL !== 'true') {
|
|
411
|
+
console.log(pc.magenta('\nš¦ Installing dependencies (pnpm)...'))
|
|
412
|
+
try {
|
|
413
|
+
execSync('pnpm install', { cwd: targetDir, stdio: 'inherit' })
|
|
414
|
+
} catch (e) {
|
|
415
|
+
console.warn(pc.yellow('Warning: pnpm install failed.'))
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
console.log(pc.green('\nā
SNAPPY Stack is ready!'))
|
|
420
|
+
console.log(`\nNext steps:\n cd ${pc.bold(projectName)}\n pnpm run dev\n`)
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.error(pc.red(`Installation failed: ${err.message || err}`))
|
|
423
|
+
} finally {
|
|
424
|
+
if (rl) rl.close()
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
main().catch((err) => {
|
|
429
|
+
console.error(pc.red(err))
|
|
430
|
+
if (rl) rl.close()
|
|
431
|
+
process.exit(1)
|
|
432
|
+
})
|