diff --git a/scripts/forge.ts b/scripts/forge.ts deleted file mode 100644 index 6563ea6aa..000000000 --- a/scripts/forge.ts +++ /dev/null @@ -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( - '', - `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() diff --git a/scripts/forge/cli/setup.ts b/scripts/forge/cli/setup.ts new file mode 100644 index 000000000..14e63fb3b --- /dev/null +++ b/scripts/forge/cli/setup.ts @@ -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( + '', + `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 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() +} diff --git a/scripts/forge/commands/dev-commands.ts b/scripts/forge/commands/dev-commands.ts new file mode 100644 index 000000000..c67d418f5 --- /dev/null +++ b/scripts/forge/commands/dev-commands.ts @@ -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 = { + 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 +} diff --git a/scripts/forge/commands/project-commands.ts b/scripts/forge/commands/project-commands.ts new file mode 100644 index 000000000..235fe95c9 --- /dev/null +++ b/scripts/forge/commands/project-commands.ts @@ -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 +} diff --git a/scripts/forge/constants/constants.ts b/scripts/forge/constants/constants.ts new file mode 100644 index 000000000..d7b1a14f1 --- /dev/null +++ b/scripts/forge/constants/constants.ts @@ -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] diff --git a/scripts/forge/index.ts b/scripts/forge/index.ts new file mode 100644 index 000000000..2b3dd4e4f --- /dev/null +++ b/scripts/forge/index.ts @@ -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) +} diff --git a/scripts/forge/types/index.ts b/scripts/forge/types/index.ts new file mode 100644 index 000000000..8a1801837 --- /dev/null +++ b/scripts/forge/types/index.ts @@ -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 +} + +export interface ConcurrentServiceConfig { + name: string + command: string + cwd?: string + color?: string +} + +// Re-export types from constants +export type { + ProjectType, + ServiceType, + CommandType +} from '../constants/constants' diff --git a/scripts/forge/utils/helpers.ts b/scripts/forge/utils/helpers.ts new file mode 100644 index 000000000..bbd148931 --- /dev/null +++ b/scripts/forge/utils/helpers.ts @@ -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( + 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.`) +}