@@ -23,6 +23,134 @@ export interface DumpOptions {
23
23
skipGlobal ?: boolean // Skip global package processing for testing
24
24
}
25
25
26
+ /**
27
+ * Check if packages are installed in the given environment directory
28
+ */
29
+ function checkMissingPackages ( packages : string [ ] , envDir : string ) : string [ ] {
30
+ if ( packages . length === 0 )
31
+ return [ ]
32
+
33
+ const pkgsDir = path . join ( envDir , 'pkgs' )
34
+ if ( ! fs . existsSync ( pkgsDir ) ) {
35
+ return packages // All packages are missing if pkgs dir doesn't exist
36
+ }
37
+
38
+ const missingPackages : string [ ] = [ ]
39
+
40
+ for ( const packageSpec of packages ) {
41
+ // Parse package spec (e.g., "php@^8.4.0" -> "php")
42
+ const [ packageName ] = packageSpec . split ( '@' )
43
+
44
+ const packageDir = path . join ( pkgsDir , packageName )
45
+ if ( ! fs . existsSync ( packageDir ) ) {
46
+ missingPackages . push ( packageSpec )
47
+ continue
48
+ }
49
+
50
+ // Check if any version of this package is installed
51
+ try {
52
+ const versionDirs = fs . readdirSync ( packageDir , { withFileTypes : true } )
53
+ . filter ( entry => entry . isDirectory ( ) && entry . name . startsWith ( 'v' ) )
54
+
55
+ if ( versionDirs . length === 0 ) {
56
+ missingPackages . push ( packageSpec )
57
+ }
58
+ }
59
+ catch {
60
+ missingPackages . push ( packageSpec )
61
+ }
62
+ }
63
+
64
+ return missingPackages
65
+ }
66
+
67
+ /**
68
+ * Check if environment needs packages installed based on what's actually missing
69
+ */
70
+ function needsPackageInstallation ( localPackages : string [ ] , globalPackages : string [ ] , envDir : string , globalEnvDir : string ) : { needsLocal : boolean , needsGlobal : boolean , missingLocal : string [ ] , missingGlobal : string [ ] } {
71
+ const missingLocal = checkMissingPackages ( localPackages , envDir )
72
+ const missingGlobal = checkMissingPackages ( globalPackages , globalEnvDir )
73
+
74
+ return {
75
+ needsLocal : missingLocal . length > 0 ,
76
+ needsGlobal : missingGlobal . length > 0 ,
77
+ missingLocal,
78
+ missingGlobal,
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Detect if this is a Laravel project and provide setup assistance
84
+ */
85
+ function detectLaravelProject ( dir : string ) : { isLaravel : boolean , suggestions : string [ ] } {
86
+ const artisanFile = path . join ( dir , 'artisan' )
87
+ const composerFile = path . join ( dir , 'composer.json' )
88
+ const appDir = path . join ( dir , 'app' )
89
+
90
+ if ( ! fs . existsSync ( artisanFile ) || ! fs . existsSync ( composerFile ) || ! fs . existsSync ( appDir ) ) {
91
+ return { isLaravel : false , suggestions : [ ] }
92
+ }
93
+
94
+ const suggestions : string [ ] = [ ]
95
+
96
+ // Check for .env file
97
+ const envFile = path . join ( dir , '.env' )
98
+ if ( ! fs . existsSync ( envFile ) ) {
99
+ const envExample = path . join ( dir , '.env.example' )
100
+ if ( fs . existsSync ( envExample ) ) {
101
+ suggestions . push ( 'Copy .env.example to .env and configure database settings' )
102
+ }
103
+ }
104
+
105
+ // Check for database configuration
106
+ if ( fs . existsSync ( envFile ) ) {
107
+ try {
108
+ const envContent = fs . readFileSync ( envFile , 'utf8' )
109
+ if ( envContent . includes ( 'DB_CONNECTION=mysql' ) && ! envContent . includes ( 'DB_PASSWORD=' ) ) {
110
+ suggestions . push ( 'Configure MySQL database credentials in .env file' )
111
+ }
112
+ if ( envContent . includes ( 'DB_CONNECTION=pgsql' ) && ! envContent . includes ( 'DB_PASSWORD=' ) ) {
113
+ suggestions . push ( 'Configure PostgreSQL database credentials in .env file' )
114
+ }
115
+ if ( envContent . includes ( 'DB_CONNECTION=sqlite' ) ) {
116
+ const dbFile = envContent . match ( / D B _ D A T A B A S E = ( .+ ) / ) ?. [ 1 ] ?. trim ( )
117
+ if ( dbFile && ! fs . existsSync ( dbFile ) ) {
118
+ suggestions . push ( `Create SQLite database file: touch ${ dbFile } ` )
119
+ }
120
+ }
121
+ }
122
+ catch {
123
+ // Ignore errors reading .env file
124
+ }
125
+ }
126
+
127
+ // Check if migrations have been run
128
+ try {
129
+ const databaseDir = path . join ( dir , 'database' )
130
+ const migrationsDir = path . join ( databaseDir , 'migrations' )
131
+ if ( fs . existsSync ( migrationsDir ) ) {
132
+ const migrations = fs . readdirSync ( migrationsDir ) . filter ( f => f . endsWith ( '.php' ) )
133
+ if ( migrations . length > 0 ) {
134
+ suggestions . push ( 'Run database migrations: php artisan migrate' )
135
+
136
+ // Check for seeders
137
+ const seedersDir = path . join ( databaseDir , 'seeders' )
138
+ if ( fs . existsSync ( seedersDir ) ) {
139
+ const seeders = fs . readdirSync ( seedersDir ) . filter ( f => f . endsWith ( '.php' ) && f !== 'DatabaseSeeder.php' )
140
+ if ( seeders . length > 0 ) {
141
+ suggestions . push ( 'Seed database with test data: php artisan db:seed' )
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+ catch {
148
+ // Ignore errors checking migrations
149
+ }
150
+
151
+ return { isLaravel : true , suggestions }
152
+ }
153
+
26
154
export async function dump ( dir : string , options : DumpOptions = { } ) : Promise < void > {
27
155
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
28
156
@@ -261,10 +389,19 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
261
389
const globalBinPath = path . join ( globalEnvDir , 'bin' )
262
390
const globalSbinPath = path . join ( globalEnvDir , 'sbin' )
263
391
264
- // If environments don't exist, trigger minimal installation
265
- if ( ! fs . existsSync ( envBinPath ) && ( localPackages . length > 0 || globalPackages . length > 0 ) ) {
266
- // Only install if we have packages and no environment
267
- await installPackagesOptimized ( localPackages , globalPackages , envDir , globalEnvDir , dryrun , effectiveQuiet )
392
+ // Check what packages are actually missing and need installation
393
+ const packageStatus = needsPackageInstallation ( localPackages , globalPackages , envDir , globalEnvDir )
394
+
395
+ // Install missing packages if any are found
396
+ if ( packageStatus . needsLocal || packageStatus . needsGlobal ) {
397
+ await installPackagesOptimized (
398
+ packageStatus . missingLocal ,
399
+ packageStatus . missingGlobal ,
400
+ envDir ,
401
+ globalEnvDir ,
402
+ dryrun ,
403
+ effectiveQuiet ,
404
+ )
268
405
}
269
406
270
407
outputShellCode ( dir , envBinPath , envSbinPath , projectHash , sniffResult , globalBinPath , globalSbinPath )
@@ -277,6 +414,16 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
277
414
await installPackagesOptimized ( localPackages , globalPackages , envDir , globalEnvDir , dryrun , effectiveQuiet )
278
415
}
279
416
417
+ // Check for Laravel project and provide helpful suggestions
418
+ const laravelInfo = detectLaravelProject ( projectDir )
419
+ if ( laravelInfo . isLaravel && laravelInfo . suggestions . length > 0 && ! effectiveQuiet ) {
420
+ console . log ( '\n🎯 Laravel project detected! Helpful commands:' )
421
+ laravelInfo . suggestions . forEach ( ( suggestion ) => {
422
+ console . log ( ` • ${ suggestion } ` )
423
+ } )
424
+ console . log ( )
425
+ }
426
+
280
427
// Output shell code if requested
281
428
if ( shellOutput ) {
282
429
const envBinPath = path . join ( envDir , 'bin' )
0 commit comments