1
1
/* eslint-disable no-console */
2
+ import { execSync } from 'node:child_process'
3
+ import { config } from '../config'
4
+ import type { PostSetupCommand } from '../types'
2
5
import crypto from 'node:crypto'
3
6
import fs from 'node:fs'
4
7
import { homedir } from 'node:os'
@@ -86,7 +89,7 @@ function needsPackageInstallation(localPackages: string[], globalPackages: strin
86
89
/**
87
90
* Detect if this is a Laravel project and provide setup assistance
88
91
*/
89
- export function detectLaravelProject ( dir : string ) : { isLaravel : boolean , suggestions : string [ ] } {
92
+ export async function detectLaravelProject ( dir : string ) : Promise < { isLaravel : boolean , suggestions : string [ ] } > {
90
93
const artisanFile = path . join ( dir , 'artisan' )
91
94
const composerFile = path . join ( dir , 'composer.json' )
92
95
const appDir = path . join ( dir , 'app' )
@@ -106,10 +109,52 @@ export function detectLaravelProject(dir: string): { isLaravel: boolean, suggest
106
109
}
107
110
}
108
111
109
- // Check for database configuration
112
+ // Check for Laravel application key and generate if missing
110
113
if ( fs . existsSync ( envFile ) ) {
111
114
try {
112
115
const envContent = fs . readFileSync ( envFile , 'utf8' )
116
+
117
+ // Check for missing or empty APP_KEY
118
+ const appKeyMatch = envContent . match ( / ^ A P P _ K E Y = ( .* ) $ / m)
119
+ const appKey = appKeyMatch ?. [ 1 ] ?. trim ( )
120
+
121
+ // Remove debug message
122
+
123
+ if ( ! appKey || appKey === '' || appKey === 'base64:' ) {
124
+ // Check if PHP and Artisan are available before attempting key generation
125
+ try {
126
+ // First verify PHP is working
127
+ execSync ( 'php --version' , { cwd : dir , stdio : 'pipe' } )
128
+
129
+ // Then verify Artisan is available
130
+ execSync ( 'php artisan --version' , { cwd : dir , stdio : 'pipe' } )
131
+
132
+ // Now generate the key
133
+ execSync ( 'php artisan key:generate --force' , {
134
+ cwd : dir ,
135
+ stdio : 'pipe'
136
+ } )
137
+
138
+ // Verify the key was generated successfully
139
+ const updatedEnvContent = fs . readFileSync ( envFile , 'utf8' )
140
+ const updatedAppKeyMatch = updatedEnvContent . match ( / ^ A P P _ K E Y = ( .* ) $ / m)
141
+ const updatedAppKey = updatedAppKeyMatch ?. [ 1 ] ?. trim ( )
142
+
143
+ if ( updatedAppKey && updatedAppKey !== '' && updatedAppKey !== 'base64:' ) {
144
+ suggestions . push ( '✅ Generated Laravel application encryption key automatically' )
145
+ } else {
146
+ suggestions . push ( '⚠️ Run: php artisan key:generate to set application encryption key' )
147
+ }
148
+ } catch ( keyGenError ) {
149
+ // If automatic generation fails, suggest manual command
150
+ suggestions . push ( '⚠️ Generate application encryption key: php artisan key:generate' )
151
+ }
152
+ } else if ( appKey && appKey . length > 10 ) {
153
+ // Key exists and looks valid
154
+ suggestions . push ( '✅ Laravel application encryption key is configured' )
155
+ }
156
+
157
+ // Check for database configuration
113
158
if ( envContent . includes ( 'DB_CONNECTION=mysql' ) && ! envContent . includes ( 'DB_PASSWORD=' ) ) {
114
159
suggestions . push ( 'Configure MySQL database credentials in .env file' )
115
160
}
@@ -152,9 +197,181 @@ export function detectLaravelProject(dir: string): { isLaravel: boolean, suggest
152
197
// Ignore errors checking migrations
153
198
}
154
199
200
+ // Execute post-setup commands if enabled
201
+ if ( config . services . frameworks . laravel . postSetupCommands . enabled ) {
202
+ const postSetupResults = await executePostSetupCommands ( dir , config . services . frameworks . laravel . postSetupCommands . commands )
203
+ suggestions . push ( ...postSetupResults )
204
+ }
205
+
155
206
return { isLaravel : true , suggestions }
156
207
}
157
208
209
+ /**
210
+ * Execute post-setup commands based on their conditions
211
+ */
212
+ async function executePostSetupCommands ( projectDir : string , commands : PostSetupCommand [ ] ) : Promise < string [ ] > {
213
+ const results : string [ ] = [ ]
214
+
215
+ for ( const command of commands ) {
216
+ try {
217
+ const shouldRun = evaluateCommandCondition ( command . condition , projectDir )
218
+
219
+ if ( ! shouldRun ) {
220
+ continue
221
+ }
222
+
223
+ if ( command . runInBackground ) {
224
+ // Run in background (fire and forget)
225
+ execSync ( command . command , { cwd : projectDir , stdio : 'pipe' } )
226
+ results . push ( `🚀 Running in background: ${ command . description } ` )
227
+ } else {
228
+ // Run synchronously
229
+ const output = execSync ( command . command , { cwd : projectDir , stdio : 'pipe' , encoding : 'utf8' } )
230
+ results . push ( `✅ ${ command . description } ` )
231
+
232
+ // Add command output if verbose
233
+ if ( config . verbose && output . trim ( ) ) {
234
+ results . push ( ` Output: ${ output . trim ( ) . split ( '\n' ) . slice ( 0 , 3 ) . join ( ', ' ) } ...` )
235
+ }
236
+ }
237
+ } catch ( error ) {
238
+ if ( command . required ) {
239
+ results . push ( `❌ Failed (required): ${ command . description } ` )
240
+ if ( error instanceof Error ) {
241
+ results . push ( ` Error: ${ error . message } ` )
242
+ }
243
+ } else {
244
+ results . push ( `⚠️ Skipped (optional): ${ command . description } ` )
245
+ if ( config . verbose && error instanceof Error ) {
246
+ results . push ( ` Reason: ${ error . message } ` )
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ return results
253
+ }
254
+
255
+ /**
256
+ * Evaluate whether a command condition is met
257
+ */
258
+ function evaluateCommandCondition ( condition : PostSetupCommand [ 'condition' ] , projectDir : string ) : boolean {
259
+ switch ( condition ) {
260
+ case 'always' :
261
+ return true
262
+ case 'never' :
263
+ return false
264
+ case 'hasUnrunMigrations' :
265
+ return hasUnrunMigrations ( projectDir )
266
+ case 'hasSeeders' :
267
+ return hasSeeders ( projectDir )
268
+ case 'needsStorageLink' :
269
+ return needsStorageLink ( projectDir )
270
+ case 'isProduction' :
271
+ return isProductionEnvironment ( projectDir )
272
+ default :
273
+ return false
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Check if there are unrun migrations
279
+ */
280
+ function hasUnrunMigrations ( projectDir : string ) : boolean {
281
+ try {
282
+ const migrationsDir = path . join ( projectDir , 'database' , 'migrations' )
283
+ if ( ! fs . existsSync ( migrationsDir ) ) {
284
+ return false
285
+ }
286
+
287
+ // Check if there are migration files
288
+ const migrationFiles = fs . readdirSync ( migrationsDir ) . filter ( file => file . endsWith ( '.php' ) )
289
+ if ( migrationFiles . length === 0 ) {
290
+ return false
291
+ }
292
+
293
+ // Try to check migration status (this requires database connection)
294
+ try {
295
+ const output = execSync ( 'php artisan migrate:status' , { cwd : projectDir , stdio : 'pipe' , encoding : 'utf8' } )
296
+ // Laravel shows pending migrations as [N] instead of [Y] for migrated ones
297
+ return output . includes ( '| N |' ) // N means Not migrated (pending)
298
+ } catch {
299
+ // If we can't check status, assume we should run migrations if migration files exist
300
+ return true
301
+ }
302
+ } catch {
303
+ return false
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Check if there are database seeders
309
+ */
310
+ function hasSeeders ( projectDir : string ) : boolean {
311
+ try {
312
+ const seedersDir = path . join ( projectDir , 'database' , 'seeders' )
313
+ if ( ! fs . existsSync ( seedersDir ) ) {
314
+ return false
315
+ }
316
+
317
+ // Check for seeder files (excluding the base DatabaseSeeder.php if it's empty)
318
+ const seederFiles = fs . readdirSync ( seedersDir ) . filter ( file => file . endsWith ( '.php' ) )
319
+ if ( seederFiles . length === 0 ) {
320
+ return false
321
+ }
322
+
323
+ // Check if DatabaseSeeder.php has actual content
324
+ const databaseSeederPath = path . join ( seedersDir , 'DatabaseSeeder.php' )
325
+ if ( fs . existsSync ( databaseSeederPath ) ) {
326
+ const content = fs . readFileSync ( databaseSeederPath , 'utf8' )
327
+ // Look for actual seeder calls (not just empty run method)
328
+ return content . includes ( '$this->call(' ) || seederFiles . length > 1
329
+ }
330
+
331
+ return seederFiles . length > 0
332
+ } catch {
333
+ return false
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Check if storage link is needed
339
+ */
340
+ function needsStorageLink ( projectDir : string ) : boolean {
341
+ try {
342
+ const publicStorageLink = path . join ( projectDir , 'public' , 'storage' )
343
+ const storageAppPublic = path . join ( projectDir , 'storage' , 'app' , 'public' )
344
+
345
+ // Need storage link if:
346
+ // 1. storage/app/public exists
347
+ // 2. public/storage doesn't exist or isn't a symlink to storage/app/public
348
+ return fs . existsSync ( storageAppPublic ) &&
349
+ ( ! fs . existsSync ( publicStorageLink ) || ! fs . lstatSync ( publicStorageLink ) . isSymbolicLink ( ) )
350
+ } catch {
351
+ return false
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Check if this is a production environment
357
+ */
358
+ function isProductionEnvironment ( projectDir : string ) : boolean {
359
+ try {
360
+ const envFile = path . join ( projectDir , '.env' )
361
+ if ( ! fs . existsSync ( envFile ) ) {
362
+ return false
363
+ }
364
+
365
+ const envContent = fs . readFileSync ( envFile , 'utf8' )
366
+ const envMatch = envContent . match ( / ^ A P P _ E N V = ( .* ) $ / m)
367
+ const appEnv = envMatch ?. [ 1 ] ?. trim ( )
368
+
369
+ return appEnv === 'production' || appEnv === 'prod'
370
+ } catch {
371
+ return false
372
+ }
373
+ }
374
+
158
375
export async function dump ( dir : string , options : DumpOptions = { } ) : Promise < void > {
159
376
const { dryrun = false , quiet = false , shellOutput = false , skipGlobal = process . env . NODE_ENV === 'test' || process . env . LAUNCHPAD_SKIP_GLOBAL_AUTO_SCAN === 'true' || process . env . LAUNCHPAD_ENABLE_GLOBAL_AUTO_SCAN !== 'true' } = options
160
377
@@ -467,7 +684,7 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
467
684
await setupProjectServices ( projectDir , sniffResult , ! effectiveQuiet )
468
685
469
686
// Check for Laravel project and provide helpful suggestions
470
- const laravelInfo = detectLaravelProject ( projectDir )
687
+ const laravelInfo = await detectLaravelProject ( projectDir )
471
688
if ( laravelInfo . isLaravel ) {
472
689
if ( laravelInfo . suggestions . length > 0 && ! effectiveQuiet ) {
473
690
console . log ( '\n🎯 Laravel project detected! Helpful commands:' )
0 commit comments