Skip to content

Commit f479d59

Browse files
committed
chore: wip
1 parent 0b3778a commit f479d59

File tree

7 files changed

+842
-7
lines changed

7 files changed

+842
-7
lines changed

packages/launchpad/src/config.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,43 @@ export const defaultConfig: LaunchpadConfig = {
7171
laravel: {
7272
enabled: process.env.LAUNCHPAD_LARAVEL_ENABLED !== 'false',
7373
autoDetect: process.env.LAUNCHPAD_LARAVEL_AUTO_DETECT !== 'false',
74+
postSetupCommands: {
75+
enabled: process.env.LAUNCHPAD_LARAVEL_POST_SETUP !== 'false',
76+
commands: [
77+
{
78+
name: 'migrate',
79+
command: 'php artisan migrate',
80+
description: 'Run database migrations',
81+
condition: 'hasUnrunMigrations',
82+
runInBackground: false,
83+
required: false,
84+
},
85+
{
86+
name: 'seed',
87+
command: 'php artisan db:seed',
88+
description: 'Seed the database with sample data',
89+
condition: 'hasSeeders',
90+
runInBackground: false,
91+
required: false,
92+
},
93+
{
94+
name: 'storage-link',
95+
command: 'php artisan storage:link',
96+
description: 'Create symbolic link for storage',
97+
condition: 'needsStorageLink',
98+
runInBackground: false,
99+
required: false,
100+
},
101+
{
102+
name: 'optimize',
103+
command: 'php artisan optimize',
104+
description: 'Optimize Laravel for production',
105+
condition: 'isProduction',
106+
runInBackground: false,
107+
required: false,
108+
},
109+
],
110+
},
74111
},
75112
stacks: {
76113
enabled: process.env.LAUNCHPAD_STACKS_ENABLED !== 'false',

packages/launchpad/src/dev/dump.ts

Lines changed: 220 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
/* eslint-disable no-console */
2+
import { execSync } from 'node:child_process'
3+
import { config } from '../config'
4+
import type { PostSetupCommand } from '../types'
25
import crypto from 'node:crypto'
36
import fs from 'node:fs'
47
import { homedir } from 'node:os'
@@ -86,7 +89,7 @@ function needsPackageInstallation(localPackages: string[], globalPackages: strin
8689
/**
8790
* Detect if this is a Laravel project and provide setup assistance
8891
*/
89-
export function detectLaravelProject(dir: string): { isLaravel: boolean, suggestions: string[] } {
92+
export async function detectLaravelProject(dir: string): Promise<{ isLaravel: boolean, suggestions: string[] }> {
9093
const artisanFile = path.join(dir, 'artisan')
9194
const composerFile = path.join(dir, 'composer.json')
9295
const appDir = path.join(dir, 'app')
@@ -106,10 +109,52 @@ export function detectLaravelProject(dir: string): { isLaravel: boolean, suggest
106109
}
107110
}
108111

109-
// Check for database configuration
112+
// Check for Laravel application key and generate if missing
110113
if (fs.existsSync(envFile)) {
111114
try {
112115
const envContent = fs.readFileSync(envFile, 'utf8')
116+
117+
// Check for missing or empty APP_KEY
118+
const appKeyMatch = envContent.match(/^APP_KEY=(.*)$/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(/^APP_KEY=(.*)$/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
113158
if (envContent.includes('DB_CONNECTION=mysql') && !envContent.includes('DB_PASSWORD=')) {
114159
suggestions.push('Configure MySQL database credentials in .env file')
115160
}
@@ -152,9 +197,181 @@ export function detectLaravelProject(dir: string): { isLaravel: boolean, suggest
152197
// Ignore errors checking migrations
153198
}
154199

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+
155206
return { isLaravel: true, suggestions }
156207
}
157208

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(/^APP_ENV=(.*)$/m)
367+
const appEnv = envMatch?.[1]?.trim()
368+
369+
return appEnv === 'production' || appEnv === 'prod'
370+
} catch {
371+
return false
372+
}
373+
}
374+
158375
export async function dump(dir: string, options: DumpOptions = {}): Promise<void> {
159376
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
160377

@@ -467,7 +684,7 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
467684
await setupProjectServices(projectDir, sniffResult, !effectiveQuiet)
468685

469686
// Check for Laravel project and provide helpful suggestions
470-
const laravelInfo = detectLaravelProject(projectDir)
687+
const laravelInfo = await detectLaravelProject(projectDir)
471688
if (laravelInfo.isLaravel) {
472689
if (laravelInfo.suggestions.length > 0 && !effectiveQuiet) {
473690
console.log('\n🎯 Laravel project detected! Helpful commands:')

packages/launchpad/src/install.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3077,7 +3077,13 @@ export async function install(packages: PackageSpec | PackageSpec[], basePath?:
30773077
console.error(`❌ Failed to install ${pkg}: ${error instanceof Error ? error.message : String(error)}`)
30783078
}
30793079
else {
3080-
console.warn(`⚠️ Warning: Failed to install ${pkg}`)
3080+
const errorMessage = error instanceof Error ? error.message : String(error)
3081+
if (errorMessage.includes('Library not loaded') || errorMessage.includes('dylib')) {
3082+
logUniqueMessage(`⚠️ Warning: Failed to install ${pkg} (library loading issue - try clearing cache)`)
3083+
}
3084+
else {
3085+
logUniqueMessage(`⚠️ Warning: Failed to install ${pkg}`)
3086+
}
30813087
}
30823088
// Continue with other packages instead of throwing
30833089
}
@@ -3271,7 +3277,12 @@ export async function install(packages: PackageSpec | PackageSpec[], basePath?:
32713277
console.error(`❌ Failed to install ${pkg}: ${errorMessage}`)
32723278
}
32733279
else {
3274-
console.warn(`⚠️ Warning: Failed to install ${pkg}`)
3280+
if (errorMessage.includes('Library not loaded') || errorMessage.includes('dylib')) {
3281+
logUniqueMessage(`⚠️ Warning: Failed to install ${pkg} (library loading issue - try clearing cache)`)
3282+
}
3283+
else {
3284+
logUniqueMessage(`⚠️ Warning: Failed to install ${pkg}`)
3285+
}
32753286
}
32763287
}
32773288
// Continue with other packages instead of throwing

packages/launchpad/src/types.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,28 @@ export interface ServiceConfig {
8282
php: PHPConfig
8383
}
8484

85+
export interface PostSetupCommandsConfig {
86+
/** Enable post-setup commands _(default: true)_ */
87+
enabled: boolean
88+
/** List of commands to run after project setup */
89+
commands: PostSetupCommand[]
90+
}
91+
92+
export interface PostSetupCommand {
93+
/** Unique name for the command */
94+
name: string
95+
/** The actual command to execute */
96+
command: string
97+
/** Human-readable description */
98+
description: string
99+
/** Condition that must be met for the command to run */
100+
condition: 'hasUnrunMigrations' | 'hasSeeders' | 'needsStorageLink' | 'isProduction' | 'always' | 'never'
101+
/** Whether to run the command in the background */
102+
runInBackground: boolean
103+
/** Whether this command is required (will cause setup to fail if it fails) */
104+
required: boolean
105+
}
106+
85107
export interface FrameworksConfig {
86108
/** Enable automatic framework detection and setup _(default: true)_ */
87109
enabled: boolean
@@ -93,6 +115,8 @@ export interface FrameworksConfig {
93115
enabled: boolean
94116
/** Automatically detect Laravel projects _(default: true)_ */
95117
autoDetect: boolean
118+
/** Post-setup commands configuration */
119+
postSetupCommands: PostSetupCommandsConfig
96120
}
97121
/** Stacks.js-specific configuration */
98122
stacks: {

0 commit comments

Comments
 (0)