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:
Melvin Chia
2025-10-10 13:20:33 +08:00
parent f578eeec90
commit 9f17cfd33b
19 changed files with 1037 additions and 24 deletions

View File

@@ -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
*/

View File

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

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

View File

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

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

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

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

View 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

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

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

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

View File

@@ -0,0 +1,3 @@
// Re-export server injection utilities
export { injectModuleRoute, removeModuleRoute } from './route-injection'
export { injectModuleSchema, removeModuleSchema } from './schema-injection'

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

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

View File

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

View File

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

View File

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

View File

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

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