mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-06-28 06:46:24 +00:00
feat(scripts): implement dev command for forge CLI and restructure the entire script
Former-commit-id: 7a1b170002b3b720bade5f247589dc0df15dc538 [formerly dfcab2d757d647b01d710e950d5dbfcdd0b3c1b5] [formerly 606b96d2c5d5c63a7cc1182bd33e389695b30b18 [formerly e1a0ea2bfc43c5fe25deef4f0981573c19b755cd]] Former-commit-id: c6374c7614ab9ce90306e69b183ccc5afb53f56c [formerly 7cad287e8f8d3ccfb3c9112594865d0e2a0b58d3] Former-commit-id: 54e9bcf1d0be858779c1cd24806789e0c17e7b0e
This commit is contained in:
@@ -1,93 +0,0 @@
|
||||
import { execSync } from 'child_process'
|
||||
import { program } from 'commander'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const TOOLS_DIR = path.join(__dirname, '../tools')
|
||||
|
||||
const PROCESS_ALLOWED = ['build', 'dev', 'types', 'lint'] as const
|
||||
|
||||
const PROJECTS_ALLOWED = Object.assign(
|
||||
{
|
||||
shared: 'shared',
|
||||
ui: 'packages/lifeforge-ui',
|
||||
client: 'client',
|
||||
server: 'server'
|
||||
},
|
||||
Object.fromEntries(
|
||||
fs
|
||||
.readdirSync(TOOLS_DIR)
|
||||
.filter(f => fs.statSync(path.join(TOOLS_DIR, f)).isDirectory())
|
||||
.map(f => [f, `tools/${f}`])
|
||||
)
|
||||
)
|
||||
|
||||
type ProcessType = (typeof PROCESS_ALLOWED)[number]
|
||||
type ProjectType = keyof typeof PROJECTS_ALLOWED
|
||||
|
||||
function executeCommand(
|
||||
processType: ProcessType,
|
||||
projects: ProjectType[]
|
||||
): void {
|
||||
const isAll = projects.includes('all' as ProjectType)
|
||||
|
||||
const finalProjects = isAll
|
||||
? (Object.keys(PROJECTS_ALLOWED) as ProjectType[])
|
||||
: projects
|
||||
|
||||
const commands = finalProjects.map(
|
||||
projectType =>
|
||||
`cd ${PROJECTS_ALLOWED[projectType]} && bun run ${processType}`
|
||||
)
|
||||
|
||||
console.log(`Running ${processType} for ${finalProjects.length} projects...`)
|
||||
|
||||
for (const command of commands) {
|
||||
console.log(`Executing command: ${command}`)
|
||||
|
||||
try {
|
||||
execSync(command, { stdio: 'inherit' })
|
||||
console.log(`Command completed: ${command}`)
|
||||
} catch {
|
||||
console.error(`Command failed: ${command}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`All projects ${processType} completed successfully.`)
|
||||
}
|
||||
|
||||
program
|
||||
.name('Lifeforge Forge')
|
||||
.description('Build and manage Lifeforge projects')
|
||||
.version('25w41')
|
||||
|
||||
// Add individual commands for each process type
|
||||
for (const processType of PROCESS_ALLOWED) {
|
||||
program
|
||||
.command(processType)
|
||||
.description(`Run ${processType} for specified projects`)
|
||||
.argument(
|
||||
'<projects...>',
|
||||
`Project names to run ${processType} on. Use 'all' for all projects. Available: all, ${Object.keys(PROJECTS_ALLOWED).join(', ')}`
|
||||
)
|
||||
.action((projects: string[]) => {
|
||||
// Validate projects
|
||||
const validProjects = [...Object.keys(PROJECTS_ALLOWED), 'all']
|
||||
|
||||
const invalidProjects = projects.filter(
|
||||
project => !validProjects.includes(project)
|
||||
)
|
||||
|
||||
if (invalidProjects.length > 0) {
|
||||
console.error(
|
||||
`Invalid project(s): ${invalidProjects.join(', ')}. Allowed projects are: all, ${Object.keys(PROJECTS_ALLOWED).join(', ')}`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
executeCommand(processType, projects as ProjectType[])
|
||||
})
|
||||
}
|
||||
|
||||
program.parse()
|
||||
62
scripts/forge/cli/setup.ts
Normal file
62
scripts/forge/cli/setup.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { program } from 'commander'
|
||||
|
||||
import { devHandler, getAvailableServices } from '../commands/dev-commands'
|
||||
import {
|
||||
createCommandHandler,
|
||||
getAvailableCommands
|
||||
} from '../commands/project-commands'
|
||||
import { PROJECTS_ALLOWED } from '../constants/constants'
|
||||
|
||||
/**
|
||||
* Sets up the CLI program with all commands
|
||||
*/
|
||||
export function setupCLI(): void {
|
||||
program
|
||||
.name('Lifeforge Forge')
|
||||
.description('Build and manage Lifeforge projects')
|
||||
.version('25w41')
|
||||
|
||||
setupProjectCommands()
|
||||
setupDevCommand()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up project commands (build, types, lint)
|
||||
*/
|
||||
function setupProjectCommands(): void {
|
||||
const availableCommands = getAvailableCommands()
|
||||
|
||||
for (const commandType of availableCommands) {
|
||||
program
|
||||
.command(commandType)
|
||||
.description(`Run ${commandType} for specified projects`)
|
||||
.argument(
|
||||
'<projects...>',
|
||||
`Project names to run ${commandType} on. Use 'all' for all projects. Available: all, ${Object.keys(PROJECTS_ALLOWED).join(', ')}`
|
||||
)
|
||||
.action(createCommandHandler(commandType))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the dev command
|
||||
*/
|
||||
function setupDevCommand(): void {
|
||||
const availableServices = getAvailableServices()
|
||||
|
||||
program
|
||||
.command('dev')
|
||||
.description('Start Lifeforge services for development')
|
||||
.argument(
|
||||
'<service>',
|
||||
`Service to start. Use all for starting db, server, and client. Available: ${availableServices.join(', ')}`
|
||||
)
|
||||
.action(devHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses command line arguments and runs the CLI
|
||||
*/
|
||||
export function runCLI(): void {
|
||||
program.parse()
|
||||
}
|
||||
146
scripts/forge/commands/dev-commands.ts
Normal file
146
scripts/forge/commands/dev-commands.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import concurrently from 'concurrently'
|
||||
|
||||
import {
|
||||
PROJECTS_ALLOWED,
|
||||
TOOLS_ALLOWED,
|
||||
VALID_SERVICES
|
||||
} from '../constants/constants'
|
||||
import type { ConcurrentServiceConfig, ServiceType } from '../types'
|
||||
import { executeCommand, validateEnvironment } from '../utils/helpers'
|
||||
|
||||
/**
|
||||
* Service command configurations
|
||||
*/
|
||||
interface ServiceConfig {
|
||||
command: string
|
||||
cwd?: () => string | undefined
|
||||
requiresEnv?: string[]
|
||||
}
|
||||
|
||||
const SERVICE_COMMANDS: Record<string, ServiceConfig> = {
|
||||
db: {
|
||||
command: './pocketbase serve',
|
||||
cwd: () => process.env.PB_DIR,
|
||||
requiresEnv: ['PB_DIR']
|
||||
},
|
||||
server: {
|
||||
command: 'cd server && bun run dev'
|
||||
},
|
||||
client: {
|
||||
command: 'cd client && bun run dev'
|
||||
},
|
||||
ui: {
|
||||
command: 'cd packages/lifeforge-ui && bun run dev'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates service configurations for concurrent execution
|
||||
*/
|
||||
const createConcurrentServices = (): ConcurrentServiceConfig[] => [
|
||||
{
|
||||
name: 'db',
|
||||
command: SERVICE_COMMANDS.db.command,
|
||||
cwd: SERVICE_COMMANDS.db.cwd?.()
|
||||
},
|
||||
{
|
||||
name: 'server',
|
||||
command: SERVICE_COMMANDS.server.command
|
||||
},
|
||||
{
|
||||
name: 'client',
|
||||
command: SERVICE_COMMANDS.client.command
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* Starts a single service based on its configuration
|
||||
*/
|
||||
function startSingleService(service: string): void {
|
||||
// Handle core services
|
||||
if (service in SERVICE_COMMANDS) {
|
||||
const config = SERVICE_COMMANDS[service]
|
||||
|
||||
if (config.requiresEnv) {
|
||||
validateEnvironment(config.requiresEnv)
|
||||
}
|
||||
|
||||
executeCommand(config.command, {
|
||||
cwd: config.cwd?.()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Handle tool services
|
||||
if (service in TOOLS_ALLOWED) {
|
||||
const projectPath =
|
||||
PROJECTS_ALLOWED[service as keyof typeof PROJECTS_ALLOWED]
|
||||
executeCommand(`cd ${projectPath} && bun run dev`)
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(`Unknown service: ${service}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts all development services concurrently
|
||||
*/
|
||||
function startAllServices(): void {
|
||||
validateEnvironment(['PB_DIR'])
|
||||
console.log('🚀 Starting all services: db, server, client...')
|
||||
|
||||
try {
|
||||
const services = createConcurrentServices()
|
||||
|
||||
concurrently(services, {
|
||||
killOthers: ['failure', 'success'],
|
||||
restartTries: 3,
|
||||
prefix: 'name',
|
||||
prefixColors: ['cyan', 'green', 'magenta']
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start all services.')
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a service is valid
|
||||
*/
|
||||
function validateService(service: string): void {
|
||||
if (!VALID_SERVICES.includes(service as any)) {
|
||||
console.error(`❌ Invalid service: ${service}`)
|
||||
console.error(`Available services: ${VALID_SERVICES.join(', ')}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main development command handler
|
||||
*/
|
||||
export function devHandler(service: string): void {
|
||||
validateService(service)
|
||||
|
||||
if (service === 'all') {
|
||||
startAllServices()
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`🚀 Starting service: ${service}...`)
|
||||
|
||||
try {
|
||||
startSingleService(service)
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to start service: ${service}`)
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of available services
|
||||
*/
|
||||
export function getAvailableServices(): readonly ServiceType[] {
|
||||
return VALID_SERVICES
|
||||
}
|
||||
66
scripts/forge/commands/project-commands.ts
Normal file
66
scripts/forge/commands/project-commands.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { PROJECTS_ALLOWED, VALID_COMMANDS } from '../constants/constants'
|
||||
import type { CommandType, ProjectType } from '../types'
|
||||
import {
|
||||
executeCommand,
|
||||
logProcessComplete,
|
||||
logProcessStart,
|
||||
resolveProjects,
|
||||
validateProjects
|
||||
} from '../utils/helpers'
|
||||
|
||||
/**
|
||||
* Executes a command for multiple projects
|
||||
*/
|
||||
export function executeProjectCommand(
|
||||
commandType: CommandType,
|
||||
projects: ProjectType[]
|
||||
): void {
|
||||
const allProjectKeys = Object.keys(PROJECTS_ALLOWED) as ProjectType[]
|
||||
const finalProjects = resolveProjects(projects, allProjectKeys)
|
||||
|
||||
logProcessStart(commandType, finalProjects)
|
||||
|
||||
for (const projectType of finalProjects) {
|
||||
const projectPath =
|
||||
PROJECTS_ALLOWED[projectType as keyof typeof PROJECTS_ALLOWED]
|
||||
const command = `cd ${projectPath} && bun run ${commandType}`
|
||||
executeCommand(command)
|
||||
}
|
||||
|
||||
logProcessComplete(commandType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates project arguments for command execution
|
||||
*/
|
||||
export function validateProjectArguments(projects: string[]): void {
|
||||
const validProjects = [...Object.keys(PROJECTS_ALLOWED), 'all']
|
||||
const validation = validateProjects(projects, validProjects)
|
||||
|
||||
if (!validation.isValid) {
|
||||
console.error(
|
||||
`❌ Invalid project(s): ${validation.invalidProjects.join(', ')}`
|
||||
)
|
||||
console.error(
|
||||
`Available projects: all, ${Object.keys(PROJECTS_ALLOWED).join(', ')}`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates command handlers for build, types, and lint commands
|
||||
*/
|
||||
export function createCommandHandler(commandType: CommandType) {
|
||||
return (projects: string[]) => {
|
||||
validateProjectArguments(projects)
|
||||
executeProjectCommand(commandType, projects as ProjectType[])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets available commands
|
||||
*/
|
||||
export function getAvailableCommands(): readonly CommandType[] {
|
||||
return VALID_COMMANDS
|
||||
}
|
||||
49
scripts/forge/constants/constants.ts
Normal file
49
scripts/forge/constants/constants.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Directory containing all tools
|
||||
*/
|
||||
export const TOOLS_DIR = path.join(__dirname, '../../../tools')
|
||||
|
||||
/**
|
||||
* Dynamically discovered tools from the tools directory
|
||||
*/
|
||||
export const TOOLS_ALLOWED = Object.fromEntries(
|
||||
fs
|
||||
.readdirSync(TOOLS_DIR)
|
||||
.filter(f => fs.statSync(path.join(TOOLS_DIR, f)).isDirectory())
|
||||
.map(f => [f, `tools/${f}`])
|
||||
)
|
||||
|
||||
/**
|
||||
* All available projects including core packages and tools
|
||||
*/
|
||||
export const PROJECTS_ALLOWED = {
|
||||
shared: 'shared',
|
||||
ui: 'packages/lifeforge-ui',
|
||||
client: 'client',
|
||||
server: 'server',
|
||||
...TOOLS_ALLOWED
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Valid services that can be started in development mode
|
||||
*/
|
||||
export const VALID_SERVICES = [
|
||||
'all',
|
||||
'db',
|
||||
'server',
|
||||
'client',
|
||||
'ui',
|
||||
...Object.keys(TOOLS_ALLOWED)
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Valid command types for project operations
|
||||
*/
|
||||
export const VALID_COMMANDS = ['build', 'types', 'lint'] as const
|
||||
|
||||
export type ProjectType = keyof typeof PROJECTS_ALLOWED
|
||||
export type ServiceType = (typeof VALID_SERVICES)[number]
|
||||
export type CommandType = (typeof VALID_COMMANDS)[number]
|
||||
28
scripts/forge/index.ts
Normal file
28
scripts/forge/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
|
||||
import { runCLI, setupCLI } from './cli/setup'
|
||||
|
||||
/**
|
||||
* Lifeforge Forge - Build and development tool for Lifeforge projects
|
||||
*
|
||||
* This tool provides commands for:
|
||||
* - Building, type-checking, and linting projects
|
||||
* - Starting development services (database, server, client, tools)
|
||||
* - Managing the entire Lifeforge monorepo ecosystem
|
||||
*/
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config({
|
||||
path: path.resolve(__dirname, '../../env/.env.local')
|
||||
})
|
||||
|
||||
// Setup and run CLI
|
||||
try {
|
||||
setupCLI()
|
||||
runCLI()
|
||||
} catch (error) {
|
||||
console.error('❌ Fatal error:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
29
scripts/forge/types/index.ts
Normal file
29
scripts/forge/types/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export interface ProjectConfig {
|
||||
path: string
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
export interface ServiceConfig extends ProjectConfig {
|
||||
command?: string
|
||||
cwd?: string
|
||||
}
|
||||
|
||||
export interface CommandExecutionOptions {
|
||||
stdio?: 'inherit' | 'pipe'
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface ConcurrentServiceConfig {
|
||||
name: string
|
||||
command: string
|
||||
cwd?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
// Re-export types from constants
|
||||
export type {
|
||||
ProjectType,
|
||||
ServiceType,
|
||||
CommandType
|
||||
} from '../constants/constants'
|
||||
90
scripts/forge/utils/helpers.ts
Normal file
90
scripts/forge/utils/helpers.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { execSync } from 'child_process'
|
||||
|
||||
import type { CommandExecutionOptions } from '../types'
|
||||
|
||||
/**
|
||||
* Validates if the provided projects are valid
|
||||
*/
|
||||
export function validateProjects(
|
||||
projects: string[],
|
||||
validProjects: string[]
|
||||
): { isValid: boolean; invalidProjects: string[] } {
|
||||
const invalidProjects = projects.filter(
|
||||
project => !validProjects.includes(project)
|
||||
)
|
||||
|
||||
return {
|
||||
isValid: invalidProjects.length === 0,
|
||||
invalidProjects
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves project list, handling 'all' keyword
|
||||
*/
|
||||
export function resolveProjects<T extends string>(
|
||||
projects: string[],
|
||||
allProjects: T[]
|
||||
): T[] {
|
||||
const isAll = projects.includes('all')
|
||||
return isAll ? allProjects : (projects as T[])
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a shell command with proper error handling
|
||||
*/
|
||||
export function executeCommand(
|
||||
command: string,
|
||||
options: CommandExecutionOptions = {}
|
||||
): void {
|
||||
try {
|
||||
console.log(`📋 Executing: ${command}`)
|
||||
execSync(command, {
|
||||
stdio: 'inherit',
|
||||
...options
|
||||
})
|
||||
console.log(`✅ Completed: ${command}`)
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed: ${command}`)
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates environment variables
|
||||
*/
|
||||
export function validateEnvironment(requiredVars: string[]): void {
|
||||
const missingVars = requiredVars.filter(varName => !process.env[varName])
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
console.error(
|
||||
`❌ Error: Missing required environment variables: ${missingVars.join(', ')}`
|
||||
)
|
||||
console.error('Please set them in your .env file.')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats project list for display
|
||||
*/
|
||||
export function formatProjectList(projects: string[]): string {
|
||||
return projects.join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a process start message
|
||||
*/
|
||||
export function logProcessStart(processType: string, projects: string[]): void {
|
||||
console.log(
|
||||
`🚀 Running ${processType} for ${projects.length} project(s): ${formatProjectList(projects)}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a process completion message
|
||||
*/
|
||||
export function logProcessComplete(processType: string): void {
|
||||
console.log(`🎉 All projects ${processType} completed successfully.`)
|
||||
}
|
||||
Reference in New Issue
Block a user