mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-06-28 06:46:24 +00:00
feat(cli): implement module management commands (add, remove, list) and enhance CLI logging
Former-commit-id: a27df1846c1fbc898c62a97e34f5f30eaa25325d [formerly ffc7fa7d80b1036f7f0fb4021fc166a58c413d40] [formerly e65c86c6674076e461d8cb804c009161aad7006d [formerly 64368a36e380a93f2d62808d6255826f11da9fee]] Former-commit-id: c0efbb95a1243aa8c1316a34ce852c98b9b0cd66 [formerly 2c7aa0860c82d25d90901e89a7d70f161308d027] Former-commit-id: c3e346e2d6b78be92c66c348e5f9df003a15dadf
This commit is contained in:
@@ -2,6 +2,7 @@ import { program } from 'commander'
|
||||
import fs from 'fs'
|
||||
|
||||
import { devHandler, getAvailableServices } from '../commands/dev-commands'
|
||||
import * as moduleHandlers from '../commands/module-commands'
|
||||
import {
|
||||
createCommandHandler,
|
||||
getAvailableCommands
|
||||
@@ -23,6 +24,7 @@ export function setupCLI(): void {
|
||||
|
||||
setupProjectCommands()
|
||||
setupDevCommand()
|
||||
setupModulesCommand()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +46,7 @@ function setupProjectCommands(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the dev command
|
||||
* Sets up the dev command for starting services in development mode
|
||||
*/
|
||||
function setupDevCommand(): void {
|
||||
const availableServices = getAvailableServices()
|
||||
@@ -59,6 +61,32 @@ function setupDevCommand(): void {
|
||||
.action(devHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up commands for managing modules
|
||||
*/
|
||||
function setupModulesCommand(): void {
|
||||
const command = program
|
||||
.command('modules')
|
||||
.description('Manage Lifeforge modules')
|
||||
.argument(
|
||||
'[action]',
|
||||
'Action to perform on modules. Available: list, add, remove'
|
||||
)
|
||||
|
||||
command.command('list').action(moduleHandlers.listModulesHandler)
|
||||
command
|
||||
.command('add')
|
||||
.argument('<module>', 'Module to add, e.g., lifeforge-app/wallet')
|
||||
.action(moduleHandlers.addModuleHandler)
|
||||
command
|
||||
.command('remove')
|
||||
.argument(
|
||||
'[module]',
|
||||
'Module to remove, e.g., wallet (optional, will show list if not provided)'
|
||||
)
|
||||
.action(moduleHandlers.removeModuleHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses command line arguments and runs the CLI
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
killExistingProcess,
|
||||
validateEnvironment
|
||||
} from '../utils/helpers'
|
||||
import { CLILoggingService } from '../utils/logging'
|
||||
|
||||
/**
|
||||
* Service command configurations
|
||||
@@ -102,7 +103,7 @@ function startSingleService(service: string): void {
|
||||
*/
|
||||
function startAllServices(): void {
|
||||
validateEnvironment(['PB_DIR'])
|
||||
console.log('🚀 Starting all services: db, server, client...')
|
||||
CLILoggingService.info('Starting all services: db, server, client...')
|
||||
|
||||
try {
|
||||
const services = createConcurrentServices()
|
||||
@@ -120,8 +121,8 @@ function startAllServices(): void {
|
||||
prefixColors: ['cyan', 'green', 'magenta']
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start all services.')
|
||||
console.error(error)
|
||||
CLILoggingService.error('Failed to start all services.')
|
||||
CLILoggingService.error(`${error}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
@@ -131,8 +132,8 @@ function startAllServices(): void {
|
||||
*/
|
||||
function validateService(service: string): void {
|
||||
if (!VALID_SERVICES.includes(service as any)) {
|
||||
console.error(`❌ Invalid service: ${service}`)
|
||||
console.error(`Available services: ${VALID_SERVICES.join(', ')}`)
|
||||
CLILoggingService.error(`Invalid service: ${service}`)
|
||||
CLILoggingService.error(`Available services: ${VALID_SERVICES.join(', ')}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
@@ -148,13 +149,13 @@ export function devHandler(service: string): void {
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`🚀 Starting service: ${service}...`)
|
||||
CLILoggingService.info(`Starting service: ${service}...`)
|
||||
|
||||
try {
|
||||
startSingleService(service)
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to start service: ${service}`)
|
||||
console.error(error)
|
||||
CLILoggingService.error(`Failed to start service: ${service}`)
|
||||
CLILoggingService.error(`${error}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
193
scripts/forge/commands/module-commands/commands/add-module.ts
Normal file
193
scripts/forge/commands/module-commands/commands/add-module.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import { executeCommand, validateFilePaths } from '../../../utils/helpers'
|
||||
import { CLILoggingService } from '../../../utils/logging'
|
||||
import {
|
||||
MODULE_STRUCTURE_REQUIREMENTS,
|
||||
type ModuleInstallConfig
|
||||
} from '../utils/constants'
|
||||
import { cleanup } from '../utils/file-system'
|
||||
import { hasServerComponents, moduleExists } from '../utils/file-system'
|
||||
import {
|
||||
injectModuleRoute,
|
||||
injectModuleSchema
|
||||
} from '../utils/server-injection'
|
||||
import { createModuleConfig, validateRepositoryPath } from '../utils/validation'
|
||||
|
||||
/**
|
||||
* Clones module repository from GitHub
|
||||
*/
|
||||
function cloneModuleRepository(config: ModuleInstallConfig): void {
|
||||
try {
|
||||
executeCommand(
|
||||
`git clone ${config.repoUrl} ${config.tempDir}/${config.moduleName}`,
|
||||
{
|
||||
exitOnError: false,
|
||||
stdio: ['ignore', 'ignore', 'ignore']
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
CLILoggingService.error(
|
||||
`Failed to clone module repository. Please check if the repository exists and is public.`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the module structure
|
||||
*/
|
||||
function validateModuleStructure(config: ModuleInstallConfig): void {
|
||||
const isValid = validateFilePaths(
|
||||
MODULE_STRUCTURE_REQUIREMENTS,
|
||||
`${config.tempDir}/${config.moduleName}`
|
||||
)
|
||||
|
||||
if (!isValid) {
|
||||
CLILoggingService.error(
|
||||
'Invalid module structure. The module must contain "client" directory and package.json file.'
|
||||
)
|
||||
throw new Error('Invalid module structure')
|
||||
}
|
||||
|
||||
CLILoggingService.info(`Module structure validated.`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves module from temp directory to apps directory
|
||||
*/
|
||||
function moveModuleToApps(config: ModuleInstallConfig): void {
|
||||
executeCommand(
|
||||
`mv ${config.tempDir}/${config.moduleName} ${config.moduleDir}`
|
||||
)
|
||||
CLILoggingService.info(
|
||||
`Module ${config.author}/${config.moduleName} added successfully.`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs dependencies for the module
|
||||
*/
|
||||
function installDependencies(): void {
|
||||
CLILoggingService.info(`Installing dependencies...`)
|
||||
|
||||
try {
|
||||
executeCommand('bun install', {
|
||||
stdio: ['ignore', 'ignore', 'ignore'],
|
||||
exitOnError: false
|
||||
})
|
||||
CLILoggingService.info(`Dependencies installed successfully.`)
|
||||
} catch (error) {
|
||||
CLILoggingService.error(`Failed to install dependencies`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates database schema migrations
|
||||
*/
|
||||
function generateSchemaMigrations(): void {
|
||||
CLILoggingService.info(`Generating schema migrations...`)
|
||||
|
||||
try {
|
||||
executeCommand('bun run db:generate-migrations', {
|
||||
stdio: ['ignore', 'ignore', 'ignore'],
|
||||
exitOnError: false
|
||||
})
|
||||
CLILoggingService.info(`Schema migrations generated successfully.`)
|
||||
} catch (error) {
|
||||
CLILoggingService.warn(
|
||||
`Failed to generate schema migrations. This is normal if the module doesn't have database schemas.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes server component injection for a module
|
||||
*/
|
||||
function processServerInjection(moduleName: string): void {
|
||||
CLILoggingService.info(`Checking for server components...`)
|
||||
|
||||
const { hasServerDir, hasServerIndex } = hasServerComponents(moduleName)
|
||||
|
||||
if (!hasServerDir) {
|
||||
CLILoggingService.info(
|
||||
`No server directory found for module "${moduleName}", skipping server injection`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasServerIndex) {
|
||||
CLILoggingService.info(
|
||||
`No server index.ts found for module "${moduleName}", skipping server injection`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
CLILoggingService.info(`Injecting server imports...`)
|
||||
|
||||
try {
|
||||
injectModuleRoute(moduleName)
|
||||
} catch (error) {
|
||||
CLILoggingService.warn(`Failed to inject route for ${moduleName}: ${error}`)
|
||||
}
|
||||
|
||||
try {
|
||||
injectModuleSchema(moduleName)
|
||||
} catch (error) {
|
||||
CLILoggingService.warn(
|
||||
`Failed to inject schema for ${moduleName}: ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles adding a new module to the Lifeforge system
|
||||
*/
|
||||
export function addModuleHandler(repoPath: string): void {
|
||||
// Validate repository path format
|
||||
if (!validateRepositoryPath(repoPath)) {
|
||||
CLILoggingService.error(
|
||||
'Invalid module name. Use the format <author>/<module-name>, e.g., lifeforge-app/wallet'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const config = createModuleConfig(repoPath)
|
||||
CLILoggingService.info(`Adding module ${repoPath} from ${config.author}`)
|
||||
|
||||
// Setup temporary directory
|
||||
cleanup(config.tempDir)
|
||||
fs.mkdirSync(config.tempDir)
|
||||
|
||||
try {
|
||||
// Check if module already exists
|
||||
if (moduleExists(config.moduleName)) {
|
||||
CLILoggingService.error(
|
||||
`A module with the name "${config.moduleName}" already exists in apps/. Please remove it first if you want to re-add.`
|
||||
)
|
||||
throw new Error('Module already exists')
|
||||
}
|
||||
|
||||
// Clone and validate module
|
||||
cloneModuleRepository(config)
|
||||
validateModuleStructure(config)
|
||||
|
||||
// Install module
|
||||
moveModuleToApps(config)
|
||||
processServerInjection(config.moduleName)
|
||||
|
||||
// Install dependencies and generate migrations
|
||||
installDependencies()
|
||||
generateSchemaMigrations()
|
||||
|
||||
CLILoggingService.info(
|
||||
`Module ${repoPath} setup completed. You may now start the system by using "bun forge dev all"`
|
||||
)
|
||||
} catch (error) {
|
||||
CLILoggingService.error(`Module installation failed: ${error}`)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
cleanup(config.tempDir)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import chalk from 'chalk'
|
||||
|
||||
import { CLILoggingService } from '../../../utils/logging'
|
||||
import { getInstalledModules } from '../utils/file-system'
|
||||
|
||||
/**
|
||||
* Handles the list modules command
|
||||
*/
|
||||
export async function listModulesHandler(): Promise<void> {
|
||||
const modules = getInstalledModules()
|
||||
|
||||
if (modules.length === 0) {
|
||||
CLILoggingService.info('No modules installed yet.')
|
||||
return
|
||||
}
|
||||
|
||||
console.log()
|
||||
CLILoggingService.info(
|
||||
`Found ${modules.length} installed module${modules.length > 1 ? 's' : ''}:`
|
||||
)
|
||||
console.log()
|
||||
|
||||
modules.forEach((module, index) => {
|
||||
console.log(
|
||||
` ${chalk.cyan((index + 1).toString().padStart(2))}. ${chalk.white(module)}`
|
||||
)
|
||||
})
|
||||
|
||||
console.log()
|
||||
}
|
||||
185
scripts/forge/commands/module-commands/commands/remove-module.ts
Normal file
185
scripts/forge/commands/module-commands/commands/remove-module.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import chalk from 'chalk'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import { executeCommand } from '../../../utils/helpers'
|
||||
import { CLILoggingService } from '../../../utils/logging'
|
||||
import {
|
||||
getInstalledModules,
|
||||
hasServerComponents,
|
||||
moduleExists
|
||||
} from '../utils/file-system'
|
||||
import {
|
||||
removeModuleRoute,
|
||||
removeModuleSchema
|
||||
} from '../utils/server-injection'
|
||||
|
||||
/**
|
||||
* Removes server component references for a module
|
||||
*/
|
||||
function removeServerReferences(moduleName: string): void {
|
||||
CLILoggingService.info(`Removing server references...`)
|
||||
|
||||
try {
|
||||
removeModuleRoute(moduleName)
|
||||
} catch (error) {
|
||||
CLILoggingService.warn(`Failed to remove route for ${moduleName}: ${error}`)
|
||||
}
|
||||
|
||||
try {
|
||||
removeModuleSchema(moduleName)
|
||||
} catch (error) {
|
||||
CLILoggingService.warn(
|
||||
`Failed to remove schema for ${moduleName}: ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the module directory
|
||||
*/
|
||||
function removeModuleDirectory(moduleName: string): void {
|
||||
const moduleDir = `apps/${moduleName}`
|
||||
|
||||
if (!fs.existsSync(moduleDir)) {
|
||||
CLILoggingService.warn(`Module directory ${moduleDir} does not exist`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
executeCommand(`rm -rf ${moduleDir}`)
|
||||
CLILoggingService.info(`Removed module directory: ${moduleDir}`)
|
||||
} catch (error) {
|
||||
CLILoggingService.error(`Failed to remove module directory: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts user to select a module to remove
|
||||
*/
|
||||
async function selectModuleToRemove(): Promise<string> {
|
||||
const installedModules = getInstalledModules()
|
||||
|
||||
if (installedModules.length === 0) {
|
||||
CLILoggingService.info('No modules found to remove.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Create choices with detailed information
|
||||
const choices = installedModules.map(module => {
|
||||
const modulePath = `apps/${module}`
|
||||
const packageJsonPath = path.join(modulePath, 'package.json')
|
||||
let description = 'No description'
|
||||
|
||||
// Try to get additional info from package.json
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
||||
description = packageData.description || 'No description'
|
||||
} catch (error) {
|
||||
// If we can't read package.json, use defaults
|
||||
}
|
||||
}
|
||||
|
||||
// Check if module has server components
|
||||
const { hasServerDir, hasServerIndex } = hasServerComponents(module)
|
||||
const serverStatus =
|
||||
hasServerDir && hasServerIndex
|
||||
? chalk.green('[Server]')
|
||||
: chalk.blue('[Client only]')
|
||||
|
||||
const moduleName = chalk.cyan.bold(module)
|
||||
const moduleDescription = chalk.gray(description)
|
||||
|
||||
return {
|
||||
title: `${moduleName} - ${moduleDescription} ${serverStatus}`,
|
||||
value: module
|
||||
}
|
||||
})
|
||||
|
||||
// Add cancel option
|
||||
choices.push({
|
||||
title: chalk.red('Cancel (do not remove any module)'),
|
||||
value: '__cancel__'
|
||||
})
|
||||
|
||||
const response = await prompts({
|
||||
type: 'autocomplete',
|
||||
name: 'selectedModule',
|
||||
message: 'Which module would you like to remove?',
|
||||
choices,
|
||||
initial: 0,
|
||||
suggest: (input: string, choices: any[]) => {
|
||||
return Promise.resolve(
|
||||
choices.filter(
|
||||
choice =>
|
||||
choice.value.toLowerCase().includes(input.toLowerCase()) ||
|
||||
choice.title.toLowerCase().includes(input.toLowerCase())
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.selectedModule || response.selectedModule === '__cancel__') {
|
||||
CLILoggingService.info('Module removal cancelled.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Confirm the deletion
|
||||
const confirmResponse = await prompts({
|
||||
type: 'confirm',
|
||||
name: 'confirmRemoval',
|
||||
message: `Are you sure you want to PERMANENTLY REMOVE the "${response.selectedModule}" module?\n This action cannot be undone and will delete all module files.`,
|
||||
initial: false
|
||||
})
|
||||
|
||||
if (!confirmResponse.confirmRemoval) {
|
||||
CLILoggingService.info('Module removal cancelled.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
return response.selectedModule
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles removing a module from the Lifeforge system
|
||||
*/
|
||||
export async function removeModuleHandler(moduleName?: string): Promise<void> {
|
||||
CLILoggingService.info('Starting module removal process...')
|
||||
|
||||
// If no module name provided, show interactive selection
|
||||
if (!moduleName) {
|
||||
moduleName = await selectModuleToRemove()
|
||||
}
|
||||
|
||||
// Validate module exists
|
||||
if (!moduleExists(moduleName)) {
|
||||
CLILoggingService.error(
|
||||
`Module "${moduleName}" does not exist in apps/ directory`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
CLILoggingService.info(`Removing module: ${moduleName}`)
|
||||
|
||||
try {
|
||||
// Remove server references first
|
||||
removeServerReferences(moduleName)
|
||||
|
||||
// Remove the module directory
|
||||
removeModuleDirectory(moduleName)
|
||||
|
||||
CLILoggingService.info(`Module "${moduleName}" removed successfully.`)
|
||||
CLILoggingService.info(
|
||||
'You may need to restart the system to see the changes.'
|
||||
)
|
||||
CLILoggingService.warn(
|
||||
'Note: Database migrations are not rolled back to prevent any data loss. If you want to remove any database schemas associated with this module, please do so manually.'
|
||||
)
|
||||
} catch (error) {
|
||||
CLILoggingService.error(`Module removal failed: ${error}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
31
scripts/forge/commands/module-commands/index.ts
Normal file
31
scripts/forge/commands/module-commands/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export { addModuleHandler } from './commands/add-module'
|
||||
export { removeModuleHandler } from './commands/remove-module'
|
||||
export { listModulesHandler } from './commands/list-modules'
|
||||
|
||||
// Export utilities for use by other CLI components
|
||||
export {
|
||||
getInstalledModules,
|
||||
moduleExists,
|
||||
hasServerComponents,
|
||||
cleanup
|
||||
} from './utils/file-system'
|
||||
|
||||
export { validateRepositoryPath, createModuleConfig } from './utils/validation'
|
||||
|
||||
export { createDynamicImport } from './utils/ast-utils'
|
||||
|
||||
// Export constants for use by other CLI components
|
||||
export {
|
||||
SERVER_CONFIG,
|
||||
MODULE_STRUCTURE_REQUIREMENTS,
|
||||
AST_GENERATION_OPTIONS,
|
||||
type ModuleInstallConfig
|
||||
} from './utils/constants'
|
||||
|
||||
// Export server injection utilities
|
||||
export {
|
||||
injectModuleRoute,
|
||||
removeModuleRoute,
|
||||
injectModuleSchema,
|
||||
removeModuleSchema
|
||||
} from './utils/server-injection'
|
||||
16
scripts/forge/commands/module-commands/utils/ast-utils.ts
Normal file
16
scripts/forge/commands/module-commands/utils/ast-utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as t from '@babel/types'
|
||||
|
||||
/**
|
||||
* AST manipulation utilities for Babel transformations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a dynamic import expression for module loading
|
||||
*/
|
||||
export function createDynamicImport(modulePath: string): t.MemberExpression {
|
||||
const awaitImport = t.awaitExpression(
|
||||
t.callExpression(t.import(), [t.stringLiteral(modulePath)])
|
||||
)
|
||||
|
||||
return t.memberExpression(awaitImport, t.identifier('default'))
|
||||
}
|
||||
43
scripts/forge/commands/module-commands/utils/constants.ts
Normal file
43
scripts/forge/commands/module-commands/utils/constants.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Module installation configuration
|
||||
*/
|
||||
export interface ModuleInstallConfig {
|
||||
tempDir: string
|
||||
moduleDir: string
|
||||
author: string
|
||||
moduleName: string
|
||||
repoUrl: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration paths for server files
|
||||
*/
|
||||
export const SERVER_CONFIG = {
|
||||
ROUTES_FILE: 'server/src/core/routes/app.routes.ts',
|
||||
SCHEMA_FILE: 'server/src/core/schema.ts'
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Module structure validation requirements
|
||||
*/
|
||||
export const MODULE_STRUCTURE_REQUIREMENTS = [
|
||||
{
|
||||
path: 'client',
|
||||
type: 'directory' as const
|
||||
},
|
||||
{
|
||||
path: 'package.json',
|
||||
type: 'file' as const
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* Babel AST generation options
|
||||
*/
|
||||
export const AST_GENERATION_OPTIONS = {
|
||||
retainLines: false,
|
||||
compact: false,
|
||||
jsescOption: {
|
||||
quotes: 'single' as const
|
||||
}
|
||||
} as const
|
||||
55
scripts/forge/commands/module-commands/utils/file-system.ts
Normal file
55
scripts/forge/commands/module-commands/utils/file-system.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* File system utilities for module operations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if module already exists in the apps directory
|
||||
*/
|
||||
export function moduleExists(moduleName: string): boolean {
|
||||
return fs.existsSync(`apps/${moduleName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if module has server components
|
||||
*/
|
||||
export function hasServerComponents(moduleName: string): {
|
||||
hasServerDir: boolean
|
||||
hasServerIndex: boolean
|
||||
} {
|
||||
const serverPath = path.resolve(`apps/${moduleName}/server`)
|
||||
const serverIndexPath = path.resolve(`apps/${moduleName}/server/index.ts`)
|
||||
|
||||
return {
|
||||
hasServerDir: fs.existsSync(serverPath),
|
||||
hasServerIndex: fs.existsSync(serverIndexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets list of installed modules from apps directory
|
||||
*/
|
||||
export function getInstalledModules(): string[] {
|
||||
const appsDir = 'apps'
|
||||
|
||||
if (!fs.existsSync(appsDir)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdirSync(appsDir, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name)
|
||||
.filter(name => !name.startsWith('.')) // Exclude hidden directories
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up temporary directory
|
||||
*/
|
||||
export function cleanup(tempDir: string): void {
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmdirSync(tempDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
155
scripts/forge/commands/module-commands/utils/route-injection.ts
Normal file
155
scripts/forge/commands/module-commands/utils/route-injection.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import generate from '@babel/generator'
|
||||
import { parse } from '@babel/parser'
|
||||
import traverse from '@babel/traverse'
|
||||
import * as t from '@babel/types'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { CLILoggingService } from '../../../utils/logging'
|
||||
import { createDynamicImport } from './ast-utils'
|
||||
import { AST_GENERATION_OPTIONS, SERVER_CONFIG } from './constants'
|
||||
|
||||
/**
|
||||
* Route injection utilities for server configuration
|
||||
*/
|
||||
|
||||
/**
|
||||
* Injects a module's server route import into the app.routes.ts file
|
||||
*/
|
||||
export function injectModuleRoute(moduleName: string): void {
|
||||
const routesConfigPath = path.resolve(SERVER_CONFIG.ROUTES_FILE)
|
||||
|
||||
if (!fs.existsSync(routesConfigPath)) {
|
||||
CLILoggingService.warn(
|
||||
`Routes config file not found at ${routesConfigPath}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const routesContent = fs.readFileSync(routesConfigPath, 'utf8')
|
||||
|
||||
try {
|
||||
const ast = parse(routesContent, {
|
||||
sourceType: 'module',
|
||||
plugins: ['typescript']
|
||||
})
|
||||
|
||||
let routerObjectPath: any = null
|
||||
|
||||
// Find the forgeRouter call expression
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
if (
|
||||
t.isIdentifier(path.node.callee, { name: 'forgeRouter' }) &&
|
||||
path.node.arguments.length > 0 &&
|
||||
t.isObjectExpression(path.node.arguments[0])
|
||||
) {
|
||||
routerObjectPath = path.get('arguments')[0]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Check if module already exists and add if not
|
||||
if (routerObjectPath && t.isObjectExpression(routerObjectPath.node)) {
|
||||
const hasExistingProperty = routerObjectPath.node.properties.some(
|
||||
(prop: any) =>
|
||||
t.isObjectProperty(prop) &&
|
||||
t.isIdentifier(prop.key) &&
|
||||
prop.key.name === moduleName
|
||||
)
|
||||
|
||||
if (!hasExistingProperty) {
|
||||
const moduleImport = createDynamicImport(`@lib/${moduleName}/server`)
|
||||
const newProperty = t.objectProperty(
|
||||
t.identifier(moduleName),
|
||||
moduleImport
|
||||
)
|
||||
routerObjectPath.node.properties.push(newProperty)
|
||||
}
|
||||
}
|
||||
|
||||
const { code } = generate(ast, AST_GENERATION_OPTIONS)
|
||||
fs.writeFileSync(routesConfigPath, code)
|
||||
|
||||
CLILoggingService.info(
|
||||
`Injected route for module "${moduleName}" into ${SERVER_CONFIG.ROUTES_FILE}`
|
||||
)
|
||||
} catch (error) {
|
||||
CLILoggingService.error(
|
||||
`Failed to inject route for module "${moduleName}": ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a module's server route from the app.routes.ts file
|
||||
*/
|
||||
export function removeModuleRoute(moduleName: string): void {
|
||||
const routesConfigPath = path.resolve(SERVER_CONFIG.ROUTES_FILE)
|
||||
|
||||
if (!fs.existsSync(routesConfigPath)) {
|
||||
CLILoggingService.warn(
|
||||
`Routes config file not found at ${routesConfigPath}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const routesContent = fs.readFileSync(routesConfigPath, 'utf8')
|
||||
|
||||
try {
|
||||
const ast = parse(routesContent, {
|
||||
sourceType: 'module',
|
||||
plugins: ['typescript']
|
||||
})
|
||||
|
||||
let modified = false
|
||||
|
||||
// Find and remove the module property from forgeRouter object
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
if (
|
||||
t.isIdentifier(path.node.callee, { name: 'forgeRouter' }) &&
|
||||
path.node.arguments.length > 0 &&
|
||||
t.isObjectExpression(path.node.arguments[0])
|
||||
) {
|
||||
const routerObject = path.node.arguments[0]
|
||||
const originalLength = routerObject.properties.length
|
||||
|
||||
routerObject.properties = routerObject.properties.filter(
|
||||
(prop: any) => {
|
||||
if (
|
||||
t.isObjectProperty(prop) &&
|
||||
t.isIdentifier(prop.key) &&
|
||||
prop.key.name === moduleName
|
||||
) {
|
||||
return false // Remove this property
|
||||
}
|
||||
return true // Keep other properties
|
||||
}
|
||||
)
|
||||
|
||||
if (routerObject.properties.length < originalLength) {
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (modified) {
|
||||
const { code } = generate(ast, AST_GENERATION_OPTIONS)
|
||||
fs.writeFileSync(routesConfigPath, code)
|
||||
|
||||
CLILoggingService.info(
|
||||
`Removed route for module "${moduleName}" from ${SERVER_CONFIG.ROUTES_FILE}`
|
||||
)
|
||||
} else {
|
||||
CLILoggingService.info(
|
||||
`No route found for module "${moduleName}" in ${SERVER_CONFIG.ROUTES_FILE}`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
CLILoggingService.error(
|
||||
`Failed to remove route for module "${moduleName}": ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
161
scripts/forge/commands/module-commands/utils/schema-injection.ts
Normal file
161
scripts/forge/commands/module-commands/utils/schema-injection.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import generate from '@babel/generator'
|
||||
import { parse } from '@babel/parser'
|
||||
import traverse from '@babel/traverse'
|
||||
import * as t from '@babel/types'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { CLILoggingService } from '../../../utils/logging'
|
||||
import { AST_GENERATION_OPTIONS, SERVER_CONFIG } from './constants'
|
||||
|
||||
/**
|
||||
* Schema injection utilities for server configuration
|
||||
*/
|
||||
|
||||
/**
|
||||
* Injects a module's schema import into the schema.ts file
|
||||
*/
|
||||
export function injectModuleSchema(moduleName: string): void {
|
||||
const schemaConfigPath = path.resolve(SERVER_CONFIG.SCHEMA_FILE)
|
||||
|
||||
if (!fs.existsSync(schemaConfigPath)) {
|
||||
CLILoggingService.warn(
|
||||
`Schema config file not found at ${schemaConfigPath}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if module has a schema file first
|
||||
const moduleSchemaPath = path.resolve(`apps/${moduleName}/server/schema.ts`)
|
||||
if (!fs.existsSync(moduleSchemaPath)) {
|
||||
CLILoggingService.info(
|
||||
`No schema file found for module "${moduleName}", skipping schema injection`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const schemaContent = fs.readFileSync(schemaConfigPath, 'utf8')
|
||||
|
||||
try {
|
||||
const ast = parse(schemaContent, {
|
||||
sourceType: 'module',
|
||||
plugins: ['typescript']
|
||||
})
|
||||
|
||||
let arrayExpressionPath: any = null
|
||||
|
||||
// Find the array expression in the exported default
|
||||
traverse(ast, {
|
||||
ExportDefaultDeclaration(path) {
|
||||
if (t.isArrayExpression(path.node.declaration)) {
|
||||
arrayExpressionPath = path.get('declaration')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (arrayExpressionPath && t.isArrayExpression(arrayExpressionPath.node)) {
|
||||
// Check if module is already imported
|
||||
const hasExistingImport = arrayExpressionPath.node.elements.some(
|
||||
(element: any) =>
|
||||
t.isAwaitExpression(element) &&
|
||||
t.isCallExpression(element.argument) &&
|
||||
t.isImport(element.argument.callee) &&
|
||||
element.argument.arguments.length > 0 &&
|
||||
t.isStringLiteral(element.argument.arguments[0]) &&
|
||||
element.argument.arguments[0].value.includes(moduleName)
|
||||
)
|
||||
|
||||
if (!hasExistingImport) {
|
||||
const moduleImport = t.awaitExpression(
|
||||
t.callExpression(t.import(), [
|
||||
t.stringLiteral(`@lib/${moduleName}/server/schema`)
|
||||
])
|
||||
)
|
||||
arrayExpressionPath.node.elements.push(moduleImport)
|
||||
}
|
||||
}
|
||||
|
||||
const { code } = generate(ast, AST_GENERATION_OPTIONS)
|
||||
fs.writeFileSync(schemaConfigPath, code)
|
||||
|
||||
CLILoggingService.info(
|
||||
`Injected schema for module "${moduleName}" into ${SERVER_CONFIG.SCHEMA_FILE}`
|
||||
)
|
||||
} catch (error) {
|
||||
CLILoggingService.error(
|
||||
`Failed to inject schema for module "${moduleName}": ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a module's schema from the schema.ts file
|
||||
*/
|
||||
export function removeModuleSchema(moduleName: string): void {
|
||||
const schemaConfigPath = path.resolve(SERVER_CONFIG.SCHEMA_FILE)
|
||||
|
||||
if (!fs.existsSync(schemaConfigPath)) {
|
||||
CLILoggingService.warn(
|
||||
`Schema config file not found at ${schemaConfigPath}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const schemaContent = fs.readFileSync(schemaConfigPath, 'utf8')
|
||||
|
||||
try {
|
||||
const ast = parse(schemaContent, {
|
||||
sourceType: 'module',
|
||||
plugins: ['typescript']
|
||||
})
|
||||
|
||||
let modified = false
|
||||
|
||||
// Find and remove the module import from the array
|
||||
traverse(ast, {
|
||||
ExportDefaultDeclaration(path) {
|
||||
if (t.isArrayExpression(path.node.declaration)) {
|
||||
const arrayExpression = path.node.declaration
|
||||
const originalLength = arrayExpression.elements.length
|
||||
|
||||
arrayExpression.elements = arrayExpression.elements.filter(
|
||||
(element: any) => {
|
||||
if (
|
||||
t.isAwaitExpression(element) &&
|
||||
t.isCallExpression(element.argument) &&
|
||||
t.isImport(element.argument.callee) &&
|
||||
element.argument.arguments.length > 0 &&
|
||||
t.isStringLiteral(element.argument.arguments[0]) &&
|
||||
element.argument.arguments[0].value.includes(moduleName)
|
||||
) {
|
||||
return false // Remove this import
|
||||
}
|
||||
return true // Keep other imports
|
||||
}
|
||||
)
|
||||
|
||||
if (arrayExpression.elements.length < originalLength) {
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (modified) {
|
||||
const { code } = generate(ast, AST_GENERATION_OPTIONS)
|
||||
fs.writeFileSync(schemaConfigPath, code)
|
||||
|
||||
CLILoggingService.info(
|
||||
`Removed schema for module "${moduleName}" from ${SERVER_CONFIG.SCHEMA_FILE}`
|
||||
)
|
||||
} else {
|
||||
CLILoggingService.info(
|
||||
`No schema found for module "${moduleName}" in ${SERVER_CONFIG.SCHEMA_FILE}`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
CLILoggingService.error(
|
||||
`Failed to remove schema for module "${moduleName}": ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Re-export server injection utilities
|
||||
export { injectModuleRoute, removeModuleRoute } from './route-injection'
|
||||
export { injectModuleSchema, removeModuleSchema } from './schema-injection'
|
||||
11
scripts/forge/commands/module-commands/utils/utils.ts
Normal file
11
scripts/forge/commands/module-commands/utils/utils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// Re-export utilities from specialized modules for backward compatibility
|
||||
export { createDynamicImport } from './ast-utils'
|
||||
|
||||
export {
|
||||
cleanup,
|
||||
getInstalledModules,
|
||||
hasServerComponents,
|
||||
moduleExists
|
||||
} from './file-system'
|
||||
|
||||
export { createModuleConfig, validateRepositoryPath } from './validation'
|
||||
27
scripts/forge/commands/module-commands/utils/validation.ts
Normal file
27
scripts/forge/commands/module-commands/utils/validation.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { ModuleInstallConfig } from './constants'
|
||||
|
||||
/**
|
||||
* Validation utilities for module operations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validates module repository path format
|
||||
*/
|
||||
export function validateRepositoryPath(repoPath: string): boolean {
|
||||
return /^[\w-]+\/[\w-]+$/.test(repoPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates module installation configuration
|
||||
*/
|
||||
export function createModuleConfig(repoPath: string): ModuleInstallConfig {
|
||||
const [author, moduleName] = repoPath.split('/')
|
||||
|
||||
return {
|
||||
tempDir: '.temp',
|
||||
moduleDir: `apps/${moduleName}`,
|
||||
author,
|
||||
moduleName,
|
||||
repoUrl: `https://github.com/${author}/lifeforge-module-${moduleName}.git`
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
resolveProjects,
|
||||
validateProjects
|
||||
} from '../utils/helpers'
|
||||
import { CLILoggingService } from '../utils/logging'
|
||||
|
||||
/**
|
||||
* Executes a command for multiple projects
|
||||
@@ -38,10 +39,10 @@ export function validateProjectArguments(projects: string[]): void {
|
||||
const validation = validateProjects(projects, validProjects)
|
||||
|
||||
if (!validation.isValid) {
|
||||
console.error(
|
||||
`❌ Invalid project(s): ${validation.invalidProjects.join(', ')}`
|
||||
CLILoggingService.error(
|
||||
`Invalid project(s): ${validation.invalidProjects.join(', ')}`
|
||||
)
|
||||
console.error(
|
||||
CLILoggingService.error(
|
||||
`Available projects: all, ${Object.keys(PROJECTS_ALLOWED).join(', ')}`
|
||||
)
|
||||
process.exit(1)
|
||||
|
||||
@@ -3,6 +3,7 @@ import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
|
||||
import { runCLI, setupCLI } from './cli/setup'
|
||||
import { CLILoggingService } from './utils/logging'
|
||||
|
||||
/**
|
||||
* Lifeforge Forge - Build and development tool for Lifeforge projects
|
||||
@@ -23,6 +24,6 @@ try {
|
||||
setupCLI()
|
||||
runCLI()
|
||||
} catch (error) {
|
||||
console.error('❌ Fatal error:', error)
|
||||
CLILoggingService.error(`Fatal error: ${error}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { IOType } from 'child_process'
|
||||
|
||||
export interface ProjectConfig {
|
||||
path: string
|
||||
displayName?: string
|
||||
@@ -9,7 +11,7 @@ export interface ServiceConfig extends ProjectConfig {
|
||||
}
|
||||
|
||||
export interface CommandExecutionOptions {
|
||||
stdio?: 'inherit' | 'pipe'
|
||||
stdio?: IOType | [IOType, IOType, IOType]
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
exitOnError?: boolean
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { execSync } from 'child_process'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import type { CommandExecutionOptions } from '../types'
|
||||
import { CLILoggingService } from './logging'
|
||||
|
||||
/**
|
||||
* Validates if the provided projects are valid
|
||||
@@ -40,13 +43,13 @@ export function executeCommand(
|
||||
const cmd = typeof command === 'function' ? command() : command
|
||||
|
||||
try {
|
||||
console.log(`📋 Executing: ${cmd}`)
|
||||
CLILoggingService.info(`Executing: ${cmd}`)
|
||||
const result = execSync(cmd, {
|
||||
stdio: 'inherit',
|
||||
...options
|
||||
})
|
||||
|
||||
console.log(`✅ Completed: ${cmd}`)
|
||||
CLILoggingService.info(`Completed: ${cmd}`)
|
||||
|
||||
return result?.toString().trim()
|
||||
} catch (error) {
|
||||
@@ -54,8 +57,8 @@ export function executeCommand(
|
||||
throw error
|
||||
}
|
||||
|
||||
console.error(`❌ Failed: ${cmd}`)
|
||||
console.error(error)
|
||||
CLILoggingService.error(`Failed: ${cmd}`)
|
||||
CLILoggingService.error(`${error}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
@@ -67,10 +70,10 @@ 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(', ')}`
|
||||
CLILoggingService.error(
|
||||
`Missing required environment variables: ${missingVars.join(', ')}`
|
||||
)
|
||||
console.error('Please set them in your .env file.')
|
||||
CLILoggingService.error('Please set them in your .env file.')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
@@ -86,8 +89,8 @@ export function formatProjectList(projects: string[]): string {
|
||||
* Logs a process start message
|
||||
*/
|
||||
export function logProcessStart(processType: string, projects: string[]): void {
|
||||
console.log(
|
||||
`🚀 Running ${processType} for ${projects.length} project(s): ${formatProjectList(projects)}`
|
||||
CLILoggingService.info(
|
||||
`Running ${processType} for ${projects.length} project(s): ${formatProjectList(projects)}`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,7 +98,7 @@ export function logProcessStart(processType: string, projects: string[]): void {
|
||||
* Logs a process completion message
|
||||
*/
|
||||
export function logProcessComplete(processType: string): void {
|
||||
console.log(`🎉 All projects ${processType} completed successfully.`)
|
||||
CLILoggingService.info(`All projects ${processType} completed successfully.`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,3 +118,33 @@ export function killExistingProcess(processKeyword: string): void {
|
||||
// No existing server instance found
|
||||
}
|
||||
}
|
||||
|
||||
type PathConfig = {
|
||||
path: string
|
||||
type: 'file' | 'directory'
|
||||
}
|
||||
|
||||
export function validateFilePaths(
|
||||
paths: PathConfig[],
|
||||
basedir: string
|
||||
): boolean {
|
||||
for (const p of paths) {
|
||||
const { path: pth, type } = p
|
||||
|
||||
const fullPath = path.resolve(basedir, pth)
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const stats = fs.lstatSync(fullPath)
|
||||
|
||||
if (type === 'file' && !stats.isFile()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (type === 'directory' && !stats.isDirectory()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
37
scripts/forge/utils/logging.ts
Normal file
37
scripts/forge/utils/logging.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { LoggingService } from '@server/core/functions/logging/loggingService'
|
||||
|
||||
/**
|
||||
* CLI Logging service that wraps the server's LoggingService
|
||||
* Provides consistent logging across the entire CLI with file persistence
|
||||
*/
|
||||
export class CLILoggingService {
|
||||
private static readonly SERVICE_NAME = 'CLI'
|
||||
|
||||
/**
|
||||
* Log an informational message
|
||||
*/
|
||||
static info(message: string): void {
|
||||
LoggingService.info(message, this.SERVICE_NAME)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error message
|
||||
*/
|
||||
static error(message: string): void {
|
||||
LoggingService.error(message, this.SERVICE_NAME)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a warning message
|
||||
*/
|
||||
static warn(message: string): void {
|
||||
LoggingService.warn(message, this.SERVICE_NAME)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a debug message
|
||||
*/
|
||||
static debug(message: string): void {
|
||||
LoggingService.debug(message, this.SERVICE_NAME)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user