create-snappy 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,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,22 @@
1
+ {
2
+ "name": "create-snappy",
3
+ "version": "0.0.1",
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
+ "dependencies": {
11
+ "commander": "^14.0.3",
12
+ "ora": "^5.4.1",
13
+ "picocolors": "^1.1.1",
14
+ "prompts": "^2.4.2"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "test": "echo \"Error: no test specified\" && exit 1"
21
+ }
22
+ }
@@ -0,0 +1,559 @@
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 crypto from 'crypto'
15
+ import { Command } from 'commander'
16
+ import prompts from 'prompts'
17
+ import pc from 'picocolors'
18
+ import ora from 'ora'
19
+
20
+ const SNAPPY_LOGO = `
21
+ ███████╗███╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ██╗
22
+ ██╔════╝████╗ ██║██╔══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝
23
+ ███████╗██╔██╗ ██║███████║██████╔╝██████╔╝ ╚████╔╝
24
+ ╚════██║██║╚██╗██║██╔══██║██╔═══╝ ██╔═══╝ ╚██╔╝
25
+ ███████║██║ ╚████║██║ ██║██║ ██║ ██║
26
+ ╚══════╝╚═╝ ╚═══╝╚╚╝ ╚═╝╚═╝ ╚═╝ ╚═╝
27
+ `
28
+
29
+ const __filename = fileURLToPath(import.meta.url)
30
+ const __dirname = path.dirname(__filename)
31
+
32
+ const CONFIG_DIR = path.join(os.homedir(), '.snappy')
33
+ const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json')
34
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
35
+
36
+ // This is the Client ID for GitHub Device OAuth
37
+ const CLIENT_ID = 'Ov23liWHoGiMponaUxrc'
38
+
39
+ const rl = readline.createInterface({
40
+ input: process.stdin,
41
+ output: process.stdout,
42
+ })
43
+
44
+ const question = (query) => new Promise((resolve) => rl.question(query, resolve))
45
+
46
+ // --- Auth & Config Utilities ---
47
+
48
+ function getSavedToken() {
49
+ if (fs.existsSync(AUTH_FILE)) {
50
+ try {
51
+ const data = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf8'))
52
+ return data.access_token
53
+ } catch (e) {
54
+ return null
55
+ }
56
+ }
57
+ return null
58
+ }
59
+
60
+ function saveToken(token) {
61
+ if (!fs.existsSync(CONFIG_DIR)) {
62
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
63
+ }
64
+ fs.writeFileSync(AUTH_FILE, JSON.stringify({ access_token: token }, null, 2))
65
+ }
66
+
67
+ function getSavedConfig() {
68
+ if (fs.existsSync(CONFIG_FILE)) {
69
+ try {
70
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'))
71
+ } catch (e) {
72
+ return {}
73
+ }
74
+ }
75
+ return {}
76
+ }
77
+
78
+ function saveConfig(config) {
79
+ if (!fs.existsSync(CONFIG_DIR)) {
80
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
81
+ }
82
+ const current = getSavedConfig()
83
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...config }, null, 2))
84
+ }
85
+
86
+ function getMachineId() {
87
+ const machineIdFile = path.join(CONFIG_DIR, 'machine-id')
88
+ const hostname = os.hostname()
89
+
90
+ if (fs.existsSync(machineIdFile)) {
91
+ return {
92
+ id: fs.readFileSync(machineIdFile, 'utf8').trim(),
93
+ hostname
94
+ }
95
+ }
96
+
97
+ // Generate a stable ID based on hardware info
98
+ const platform = os.platform()
99
+ const arch = os.arch()
100
+ const cpus = os.cpus().length
101
+
102
+ const rawId = `${platform}-${arch}-${cpus}-${hostname}`
103
+ const hash = crypto.createHash('sha256').update(rawId).digest('hex').substring(0, 12)
104
+
105
+ if (!fs.existsSync(CONFIG_DIR)) {
106
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
107
+ }
108
+ fs.writeFileSync(machineIdFile, hash)
109
+ return { id: hash, hostname }
110
+ }
111
+
112
+ async function githubLogin() {
113
+ console.log('\n🔐 Authenticating with GitHub...')
114
+
115
+ try {
116
+ const response = await fetch('https://github.com/login/device/code', {
117
+ method: 'POST',
118
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
119
+ body: JSON.stringify({ client_id: CLIENT_ID, scope: 'repo read:user' }),
120
+ })
121
+
122
+ if (response.status === 404) {
123
+ throw new Error(
124
+ 'GitHub returned 404. This usually means the Client ID is invalid or the app is not registered.',
125
+ )
126
+ }
127
+
128
+ const data = await response.json()
129
+ if (data.error) throw new Error(`Auth request failed: ${data.error_description || data.error}`)
130
+
131
+ const { device_code, user_code, verification_uri, interval, expires_in } = data
132
+
133
+ console.log(`\n1. Go to: \x1b[34m${verification_uri}\x1b[0m`)
134
+ console.log(`2. Enter code: \x1b[1m\x1b[32m${user_code}\x1b[0m`)
135
+ console.log(
136
+ `\nWaiting for authorization... (Expires in ${Math.floor(expires_in / 60)} minutes)`,
137
+ )
138
+
139
+ const poll = async () => {
140
+ const res = await fetch('https://github.com/login/oauth/access_token', {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
143
+ body: JSON.stringify({
144
+ client_id: CLIENT_ID,
145
+ device_code,
146
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
147
+ }),
148
+ })
149
+
150
+ const tokenData = await res.json()
151
+
152
+ if (tokenData.error) {
153
+ if (tokenData.error === 'authorization_pending') {
154
+ await new Promise((r) => setTimeout(r, (interval || 5) * 1000))
155
+ return poll()
156
+ }
157
+ throw new Error(`Polling failed: ${tokenData.error_description || tokenData.error}`)
158
+ }
159
+
160
+ return tokenData.access_token
161
+ }
162
+
163
+ const accessToken = await poll()
164
+ saveToken(accessToken)
165
+ console.log('✅ Successfully authenticated!')
166
+ return accessToken
167
+ } catch (err) {
168
+ console.warn(`\x1b[33m\n⚠️ GitHub OAuth failed: ${err.message}\x1b[0m`)
169
+ console.log('\x1b[36mFalling back to manual token entry...\x1b[0m')
170
+ const manualToken = await question('Paste your GitHub Personal Access Token (PAT): ')
171
+
172
+ if (!manualToken || manualToken.trim() === '') {
173
+ throw new Error('No token provided. Installation aborted.')
174
+ }
175
+
176
+ saveToken(manualToken.trim())
177
+ console.log('✅ Token saved manually.')
178
+ return manualToken.trim()
179
+ }
180
+ }
181
+
182
+ // --- Main CLI ---
183
+
184
+ function getPackageManager() {
185
+ return 'pnpm'
186
+ }
187
+
188
+ async function main() {
189
+ console.log(pc.cyan(SNAPPY_LOGO))
190
+ console.log(pc.cyan('🚀 Welcome to the SNAPPY Stack Installer! (v0.1.18)'))
191
+ console.log('------------------------------------------')
192
+
193
+ const program = new Command()
194
+ program
195
+ .name('create-snappy')
196
+ .description('The official installer for the SNAPPY stack. (Private Access Required)')
197
+ .argument('[project-name]', 'Name of the project directory')
198
+ .option('-t, --template <name>', 'Template to use (portfolio)', 'portfolio')
199
+ .option('--login', 'Authenticate with GitHub to access private templates')
200
+ .option('--guided', 'Force guided setup for environment variables')
201
+ .parse(process.argv)
202
+
203
+ const options = program.opts()
204
+ let providedName = program.args[0]
205
+
206
+ if (options.login) {
207
+ try {
208
+ await githubLogin()
209
+ rl.close()
210
+ process.exit(0)
211
+ } catch (e) {
212
+ console.error(pc.red(e.message))
213
+ rl.close()
214
+ process.exit(1)
215
+ }
216
+ return
217
+ }
218
+
219
+ const savedConfig = getSavedConfig()
220
+ const hasSavedConfig = Object.keys(savedConfig).length > 0
221
+
222
+ // INTERACTIVE PROMPTS
223
+ const questions = []
224
+
225
+ if (!providedName) {
226
+ questions.push({
227
+ type: 'text',
228
+ name: 'projectName',
229
+ message: 'What is your project named?',
230
+ initial: 'my-snappy-app',
231
+ validate: (value) => (value.length > 0 ? true : 'Please enter a project name.'),
232
+ })
233
+ }
234
+
235
+ questions.push({
236
+ type: 'text',
237
+ name: 'projectDescription',
238
+ message: 'Project description?',
239
+ initial: 'A fresh SNAPPY app',
240
+ })
241
+
242
+ questions.push({
243
+ type: 'text',
244
+ name: 'authorName',
245
+ message: 'Author name?',
246
+ initial: savedConfig.authorName || 'Wicky',
247
+ })
248
+
249
+ if (!options.template) {
250
+ questions.push({
251
+ type: 'select',
252
+ name: 'template',
253
+ message: 'Which template would you like to use?',
254
+ choices: [
255
+ { title: 'Portfolio', description: 'A sleek portfolio template', value: 'portfolio' },
256
+ ],
257
+ initial: 0,
258
+ })
259
+ }
260
+
261
+ questions.push({
262
+ type: 'password',
263
+ name: 'snappyLicenseKey',
264
+ message: 'SNAPPY License Key (leave blank for trial)?',
265
+ initial: savedConfig.snappyLicenseKey || '',
266
+ })
267
+
268
+ // Guided Setup Questions
269
+ const needsGuided = options.guided || !hasSavedConfig
270
+
271
+ if (needsGuided) {
272
+ console.log(pc.yellow('\n🛠️ Guided Setup: Please enter your infrastructure details.'))
273
+ console.log(pc.gray('These will be saved to ~/.snappy/config.json for future use.\n'))
274
+
275
+ questions.push(
276
+ {
277
+ type: 'text',
278
+ name: 's3Bucket',
279
+ message: 'R2/S3 Bucket Name?',
280
+ initial: savedConfig.s3Bucket || 'snappy-production',
281
+ },
282
+ {
283
+ type: 'text',
284
+ name: 's3Region',
285
+ message: 'R2/S3 Region?',
286
+ initial: savedConfig.s3Region || 'auto',
287
+ },
288
+ {
289
+ type: 'text',
290
+ name: 's3Endpoint',
291
+ message: 'R2/S3 Endpoint URL?',
292
+ initial: savedConfig.s3Endpoint || '',
293
+ },
294
+ {
295
+ type: 'password',
296
+ name: 's3AccessKey',
297
+ message: 'R2/S3 Access Key ID?',
298
+ initial: savedConfig.s3AccessKey || '',
299
+ },
300
+ {
301
+ type: 'password',
302
+ name: 's3SecretKey',
303
+ message: 'R2/S3 Secret Access Key?',
304
+ initial: savedConfig.s3SecretKey || '',
305
+ },
306
+ )
307
+ }
308
+
309
+ const response = await prompts(questions, {
310
+ onCancel: () => {
311
+ console.log(pc.yellow('Installation cancelled.'))
312
+ process.exit(0)
313
+ },
314
+ })
315
+
316
+ // Merge responses with saved config
317
+ const config = {
318
+ ...savedConfig,
319
+ authorName: response.authorName || savedConfig.authorName,
320
+ snappyLicenseKey: response.snappyLicenseKey || savedConfig.snappyLicenseKey,
321
+ s3Bucket: response.s3Bucket || savedConfig.s3Bucket,
322
+ s3Region: response.s3Region || savedConfig.s3Region,
323
+ s3Endpoint: response.s3Endpoint || savedConfig.s3Endpoint,
324
+ s3AccessKey: response.s3AccessKey || savedConfig.s3AccessKey,
325
+ s3SecretKey: response.s3SecretKey || savedConfig.s3SecretKey,
326
+ }
327
+
328
+ // Save if it was a guided session or forced
329
+ if (needsGuided) {
330
+ saveConfig(config)
331
+ }
332
+
333
+ const projectName = providedName || response.projectName
334
+ const projectDescription = response.projectDescription
335
+ const authorName = config.authorName
336
+ const selectedTemplate = options.template || response.template || 'portfolio'
337
+
338
+ const targetDir = path.resolve(process.cwd(), projectName)
339
+
340
+ // Target directory check (but don't create yet to keep it empty for git clone)
341
+ if (fs.existsSync(targetDir)) {
342
+ const { overwrite } = await prompts({
343
+ type: 'confirm',
344
+ name: 'overwrite',
345
+ message: pc.yellow(`Directory ${projectName} already exists. Overwrite?`),
346
+ initial: false,
347
+ })
348
+
349
+ if (!overwrite) {
350
+ console.log('Aborted.')
351
+ process.exit(0)
352
+ }
353
+ fs.rmSync(targetDir, { recursive: true, force: true })
354
+ }
355
+
356
+ let finalLicenseToken = config.snappyLicenseKey;
357
+ let isTrial = false;
358
+ let { id: machineId, hostname } = getMachineId();
359
+ let machineIdToSave = machineId;
360
+ let skippedLicense = false;
361
+
362
+ if (!isTrial && (!finalLicenseToken || finalLicenseToken.trim() === '')) {
363
+ const trialResponse = await prompts({
364
+ type: 'confirm',
365
+ name: 'startTrial',
366
+ message: '🎯 Start 2-hour free trial?',
367
+ initial: true
368
+ });
369
+
370
+ if (trialResponse.startTrial) {
371
+ console.log(pc.cyan('\n⏳ Setting up 2-hour free trial...'));
372
+
373
+ try {
374
+ const res = await fetch('https://snappycore.wicky.id/api/trial', {
375
+ method: 'POST',
376
+ headers: { 'Content-Type': 'application/json' },
377
+ body: JSON.stringify({ machineId, hostname })
378
+ });
379
+
380
+ const data = await res.json();
381
+ if (res.ok && data.token) {
382
+ finalLicenseToken = data.token;
383
+ isTrial = true;
384
+ machineIdToSave = machineId;
385
+ } else {
386
+ console.error(pc.red(`\n❌ Trial generation failed: ${data.error || 'Unknown error'}`));
387
+ console.log(pc.gray('The installer cannot proceed without a valid trial license.'));
388
+ process.exit(1);
389
+ }
390
+ } catch (err) {
391
+ console.error(pc.red(`\n❌ Could not reach licensing server: ${err.message}`));
392
+ process.exit(1);
393
+ }
394
+ } else {
395
+ skippedLicense = true;
396
+ }
397
+ }
398
+
399
+ // (Removed premature file writing)
400
+
401
+ try {
402
+ // 1. Initialize from repository
403
+ const repoUrl = 'https://github.com/snappy-stack/snappy.git'
404
+ const cloneSpinner = ora(`Cloning template (${selectedTemplate})...`).start()
405
+
406
+ try {
407
+ // Use inherit to see actual errors if it fails
408
+ cloneSpinner.stop()
409
+ execSync(`git clone --depth 1 -b ${selectedTemplate} ${repoUrl} "${targetDir}"`, {
410
+ stdio: 'inherit',
411
+ })
412
+
413
+ // Template already has correct payload.config.ts — no patching needed.
414
+ console.log(pc.green('✔ Project cloned successfully.'))
415
+ } catch (err) {
416
+ console.error(pc.red("\n❌ Installation failed during project cloning."))
417
+ console.error(pc.gray(`Template: ${selectedTemplate}`))
418
+ console.error(pc.gray(`Repository: ${repoUrl}`))
419
+ throw err
420
+ }
421
+
422
+ // Now safe to write metadata files
423
+ if (machineIdToSave) {
424
+ fs.writeFileSync(path.join(targetDir, '.snappy-machine-id'), machineIdToSave);
425
+ }
426
+
427
+ if (fs.existsSync(path.join(targetDir, '.git'))) {
428
+ fs.rmSync(path.join(targetDir, '.git'), { recursive: true, force: true })
429
+ }
430
+ console.log(pc.green('✅ Project initialized from secure source.'))
431
+
432
+ // 2. Customize package.json and README.md
433
+ console.log('\n📝 Customizing project files...')
434
+ const pkgPath = path.join(targetDir, 'package.json')
435
+ if (fs.existsSync(pkgPath)) {
436
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
437
+ pkg.name = projectName.toLowerCase().replace(/\s+/g, '-')
438
+ pkg.description = projectDescription || pkg.description
439
+ pkg.author = authorName || pkg.author
440
+ if (pkg.bin) delete pkg.bin
441
+
442
+ // FIX LEXICAL MISMATCH - FORCE 0.41.0
443
+ if (pkg.dependencies) {
444
+ const targetLexical = "0.35.0";
445
+ console.log(pc.yellow(`🔄 Forcing Lexical dependencies to ${targetLexical}...`));
446
+
447
+ // Fix main dependency
448
+ pkg.dependencies.lexical = targetLexical;
449
+
450
+ // Update @snappy-stack/core to latest
451
+ if (pkg.dependencies['@snappy-stack/core']) {
452
+ pkg.dependencies['@snappy-stack/core'] = '^0.1.7';
453
+ }
454
+
455
+ // Fix all @lexical/* sub-dependencies
456
+ Object.keys(pkg.dependencies).forEach(dep => {
457
+ if (dep.startsWith('@lexical/')) {
458
+ pkg.dependencies[dep] = targetLexical;
459
+ }
460
+ });
461
+
462
+ // Add overrides for pnpm/npm to be absolutely sure
463
+ pkg.overrides = { ...pkg.overrides, lexical: targetLexical };
464
+ pkg.resolutions = { ...pkg.resolutions, lexical: targetLexical }; // For yarn
465
+ pkg.pnpm = {
466
+ ...pkg.pnpm,
467
+ overrides: { ...pkg.pnpm?.overrides, lexical: targetLexical }
468
+ };
469
+ }
470
+
471
+ // CLEANUP LOCKFILES
472
+ ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock'].forEach(lock => {
473
+ const lockPath = path.join(targetDir, lock);
474
+ if (fs.existsSync(lockPath)) fs.unlinkSync(lockPath);
475
+ });
476
+
477
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2))
478
+ }
479
+
480
+ const readmePath = path.join(targetDir, 'README.md')
481
+ if (fs.existsSync(readmePath)) {
482
+ const readmeContent = `# ${projectName}\n\n${projectDescription}\n\nGenerated with \`create-snappy\`.`
483
+ fs.writeFileSync(readmePath, readmeContent)
484
+ }
485
+ console.log(pc.green('✅ Project details injected.'))
486
+
487
+ // 3. Configure .env
488
+ console.log('\n🛠️ Configuring environment variables...')
489
+
490
+ const finalS3Bucket = config.s3Bucket
491
+ const finalS3Endpoint = config.s3Endpoint
492
+ const finalS3AccessKey = config.s3AccessKey
493
+ const finalS3SecretKey = config.s3SecretKey
494
+ const finalS3Region = config.s3Region
495
+
496
+ const payloadSecret = crypto.randomBytes(32).toString('hex')
497
+
498
+ const envContent = `# SNAPPY STACK - Zero-Config Environment
499
+ SNAPPY_LICENSE_TOKEN="${finalLicenseToken || ''}"
500
+ SNAPPY_API_URL="https://snappycore.wicky.id"
501
+
502
+ # Payload CMS
503
+ PAYLOAD_SECRET="${payloadSecret}"
504
+ PUBLIC_FRONTEND_URL="http://localhost:3000"
505
+
506
+ # R2 Storage Configuration (Isolated via SNAPPY)
507
+ S3_BUCKET="${finalS3Bucket}"
508
+ NEXT_PUBLIC_S3_BUCKET="${finalS3Bucket}"
509
+ S3_REGION="${finalS3Region}"
510
+ S3_ENDPOINT="${finalS3Endpoint}"
511
+ S3_ACCESS_KEY_ID="${finalS3AccessKey}"
512
+ S3_SECRET_ACCESS_KEY="${finalS3SecretKey}"
513
+ `
514
+ fs.writeFileSync(path.join(targetDir, '.env'), envContent)
515
+ console.log(pc.green('✅ .env generated.\n'))
516
+
517
+ if (process.env.SKIP_INSTALL !== 'true') {
518
+ const pm = getPackageManager()
519
+ const installCmd = pm === 'npm' ? 'npm install' : `${pm} install`
520
+ const installSpinner = ora(`Installing dependencies using ${pm}...`).start()
521
+ try {
522
+ execSync(installCmd, { cwd: targetDir, stdio: 'ignore' })
523
+ installSpinner.succeed('Dependencies installed successfully.')
524
+ } catch (e) {
525
+ installSpinner.fail('Failed to install dependencies.')
526
+ console.warn(pc.yellow(`Warning: ${installCmd} failed.`))
527
+ }
528
+ }
529
+
530
+ const pmRun = getPackageManager() === 'npm' ? 'npm run dev' : `${getPackageManager()} run dev`
531
+
532
+ if (skippedLicense) {
533
+ console.log(pc.red('\n ──────────────────────────────────'));
534
+ console.log(` 🔑 ${pc.bold(pc.white('Add your license key to .env'))}`);
535
+ console.log(pc.gray(' SNAPPY_LICENSE_TOKEN=sk_snappy_...'));
536
+ console.log('');
537
+ console.log(` 📡 ${pc.cyan('Get your key: wicky.id')}`);
538
+ console.log(` 📖 ${pc.cyan('Docs: snappycore.wicky.id')}`);
539
+ console.log(pc.red(' ──────────────────────────────────'));
540
+ console.log(pc.bold(pc.red('\n THERE IS NO MERCY IN PRODUCTION 👹 \n')));
541
+ } else if (isTrial) {
542
+ console.log(pc.green('\n✅ 2-hour free trial active! Your trial license has been added to .env.'));
543
+ }
544
+
545
+ console.log(pc.green('\n✅ SNAPPY Stack is perfectly prepared and ready to launch!'))
546
+ console.log(`\nNext steps:\n cd ${pc.bold(projectName)}\n ${pmRun}\n`)
547
+
548
+ } catch (err) {
549
+ console.error(pc.red(`Installation failed: ${err.message || err}`))
550
+ } finally {
551
+ if (rl) rl.close()
552
+ }
553
+ }
554
+
555
+ main().catch((err) => {
556
+ console.error(pc.red(err))
557
+ if (rl) rl.close()
558
+ process.exit(1)
559
+ })