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:
Melvin Chia
2025-10-07 17:24:56 +08:00
parent f0b715920f
commit cbeaaaa0ea
8 changed files with 470 additions and 93 deletions

View File

@@ -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()

View 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()
}

View 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
}

View 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
}

View 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
View 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)
}

View 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'

View 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.`)
}