Merge pull request #96 from Lifeforge-app/fix/cli

Add Features to Connect Pocketbase in Local Network & Optimize Debugging
This commit is contained in:
Melvin Chia
2026-02-04 22:31:40 +08:00
committed by GitHub
5 changed files with 96 additions and 36 deletions

View File

@@ -1,9 +1,10 @@
import fs from 'fs'
import { PB_BINARY_PATH, PB_DIR, PB_KWARGS } from '@/constants/db'
import { PB_BINARY_PATH, PB_DIR, PB_KWARGS, PB_HOST, PB_PORT } from '@/constants/db'
import executeCommand from '@/utils/commands'
import { checkPortInUse, delay, killExistingProcess } from '@/utils/helpers'
import { checkAddressInUse, checkPortInUse, delay, killExistingProcess } from '@/utils/helpers'
import logger from '@/utils/logger'
import chalk from 'chalk'
/**
* Service command configurations
@@ -17,22 +18,16 @@ interface ServiceConfig {
export const SERVICE_COMMANDS: Record<string, ServiceConfig> = {
db: {
command: async () => {
const killedProcess = killExistingProcess('./pocketbase serve')
if (killedProcess) {
await delay(2000)
}
if (checkPortInUse(8090)) {
logger.error(
'No Pocketbase instance found running, but port 8090 is already in use.'
)
process.exit(1)
}
if (checkAddressInUse(PB_HOST, PB_PORT)) {
logger.error(
`Database address ${chalk.blue(`${PB_HOST}:${PB_PORT}`)} is already in use.`
)
process.exit(1)
}
if (!fs.existsSync(PB_BINARY_PATH)) {
logger.error(
`PocketBase binary does not exist: ${PB_BINARY_PATH}. Please run "bun forge db init" to initialize the database.`
`PocketBase binary does not exist: ${chalk.blue(PB_BINARY_PATH)}. Please run "bun forge db init" to initialize the database.`
)
process.exit(1)
}

View File

@@ -1,3 +1,4 @@
import chalk from 'chalk'
import concurrently from 'concurrently'
import { PROJECTS } from '@/commands/project/constants/projects'
@@ -30,7 +31,9 @@ export async function startSingleService(
const cwd = config.cwd instanceof Function ? config.cwd() : config.cwd
executeCommand(command, { cwd }, extraArgs)
logger.debug(`Current Working Directory: ${chalk.blue(cwd)}`)
executeCommand(command, { cwd, stdio: 'inherit' }, extraArgs)
return
}

View File

@@ -26,10 +26,17 @@ export const PB_BINARY_PATH = path.resolve(
process.env.PB_BINARY_PATH || `${PB_DIR}/pocketbase`
)
// Remove http:// prefix if present
export const PB_URL = getEnvVar('PB_HOST').replace(/^http:\/\//, '')
// Extract host from PB_HOST
export const [PB_HOST, PB_PORT] = PB_URL.split(':')
export const PB_KWARGS = [
`--dir=${PB_DATA_DIR}`,
`--migrationsDir=${PB_MIGRATIONS_DIR}`,
'--automigrate=0'
'--automigrate=0',
`--http ${PB_URL || 'localhost:8090'}`
]
// Straightaway exit if PB_DIR is not accessible (skip in Docker mode)

View File

@@ -6,6 +6,7 @@ import { ROOT_DIR } from '@/constants/constants'
import logger from './logger'
import { addDependency, removeDependency } from './packageJson'
import chalk from 'chalk'
interface CommandExecutionOptions {
stdio?: IOType | [IOType, IOType, IOType]
@@ -34,12 +35,12 @@ export default function executeCommand(
cmd = typeof command === 'function' ? command() : command
} catch (error) {
logger.error(`Failed to generate command.`)
logger.debug(`Error details: ${error}`)
logger.debug(`Error details: ${chalk.grey(String(error))}`)
process.exit(1)
}
try {
logger.debug(`Executing: ${cmd}`)
logger.debug(`Executing command ${chalk.blue(cmd)} with arguments: ${chalk.blue(_arguments.length ? _arguments.join(' ') : `${chalk.red('none')}`)}`)
const [toBeExecuted, ...args] = cmd.split(' ')
@@ -50,14 +51,12 @@ export default function executeCommand(
stdio: options.stdio ?? 'pipe'
})
if (logger.level === 'debug') {
if (result.stdout) {
logger.debug(result.stdout.toString())
}
if (result.stdout) {
logger.debug(chalk.grey(result.stdout.toString()))
}
if (result.stderr) {
logger.debug(result.stderr.toString())
}
if (result.stderr) {
logger.debug(chalk.grey(result.stderr.toString()))
}
if (result.error) {
@@ -69,7 +68,7 @@ export default function executeCommand(
}
if (!options.stdio || options.stdio === 'pipe') {
logger.debug(`Completed: ${cmd}`)
logger.debug(`Command Completed: ${chalk.blue(cmd)}, exit code: ${chalk.blue(String(result.status))}`)
}
return result.stdout?.toString().trim() || ''
@@ -78,8 +77,8 @@ export default function executeCommand(
throw error
}
logger.error(`Command execution failed: ${cmd}`)
logger.debug(`Error details: ${error}`)
logger.error(`Command execution failed: ${chalk.blue(cmd)}`)
logger.debug(`Error details: ${chalk.grey(String(error))}`)
process.exit(1)
}
}
@@ -113,7 +112,7 @@ export function installPackage(
fs.rmSync(targetDir, { recursive: true, force: true })
}
logger.debug(`Installing ${fullName} from registry...`)
logger.debug(`Installing ${chalk.blue(fullName)} from registry...`)
executeCommand(`bun add ${fullName}@latest --ignore-scripts`, {
cwd: ROOT_DIR
@@ -122,12 +121,11 @@ export function installPackage(
const installedPath = path.join(ROOT_DIR, 'node_modules', fullName)
if (!fs.existsSync(installedPath)) {
logger.error(`Failed to find installed package at ${installedPath}`)
logger.error(`Failed to find installed package at ${chalk.blue(installedPath)}`)
process.exit(1)
}
logger.debug(`Copying ${fullName} to ${targetDir}...`)
logger.debug(`Copying ${chalk.blue(fullName)} to ${chalk.blue(targetDir)}...`)
fs.cpSync(installedPath, targetDir, { recursive: true, dereference: true })
// Add to target package.json (apps or locales)

View File

@@ -1,3 +1,4 @@
import chalk from 'chalk'
import prompts from 'prompts'
import executeCommand from './commands'
@@ -60,7 +61,7 @@ export function getEnvVar(varName: string, fallback?: string): string {
return fallback
}
logger.error(`Missing required environment variable: ${varName}`)
logger.error(`Missing required environment variable: ${chalk.red(varName)}`)
process.exit(1)
}
@@ -77,7 +78,9 @@ export function killExistingProcess(
if (typeof processKeywordOrPID === 'number') {
process.kill(processKeywordOrPID)
logger.debug(`Killed process with PID: ${String(processKeywordOrPID)}`)
logger.debug(
`Killed process with PID: ${chalk.blue(String(processKeywordOrPID))}`
)
return
}
@@ -92,7 +95,7 @@ export function killExistingProcess(
})
logger.debug(
`Killed process matching keyword: ${processKeywordOrPID} (PID: ${serverInstance})`
`Killed process matching keyword: ${chalk.blue(processKeywordOrPID)} (PID: ${chalk.blue(serverInstance)})`
)
return parseInt(serverInstance, 10)
@@ -122,6 +125,60 @@ export function checkPortInUse(port: number): boolean {
}
}
/**
* Checks if a specific port is currently in use.
*
* @param port - The port number to check
* @returns True if the port is in use, false otherwise
*/
export function checkAddressInUse(address: string, port: string): boolean {
logger.debug(
`Checking if address ${chalk.blue(address)}:${chalk.blue(port)} is in use...`
)
try {
executeCommand('nc', { exitOnError: false }, ['-zv', address, port])
const ssOutput = executeCommand('ss', { exitOnError: false }, [
'-tlnp',
'src',
`${address}:${port}`
])
const lines = ssOutput.trim().split('\n')
if (lines.length > 1) {
const processLine = lines[1]
const processMatch = processLine.match(
/users:\(\("([^"]+)",pid=(\d+),fd=\d+\)\)/
)
if (processMatch) {
const processName = processMatch[1]
const pid = processMatch[2]
logger.error(
`Address ${chalk.blue(address)}:${chalk.blue(port)} is in use by process: ${chalk.blue(processName)} (PID: ${chalk.blue(pid)})`
)
} else {
logger.error(
`Address ${chalk.blue(address)}:${chalk.blue(port)} is in use, but could not parse process info.`
)
}
} else {
logger.error(
`Address ${chalk.blue(address)}:${chalk.blue(port)} is in use, but no process info found.`
)
}
return true
} catch {
return false
}
}
/**
* Creates a promise that resolves after the specified delay.
*