diff --git a/README.md b/README.md index 5d4684f..af70340 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ numux numux init # Create a starter numux.config.ts numux validate # Validate config and show process dependency graph numux exec [--] # Run a command in a process's environment +numux logs [name] # Open log directory or view a process log numux completions # Generate shell completions (bash, zsh, fish) ``` @@ -85,6 +86,13 @@ numux exec api -- npx prisma migrate numux exec web npm run build ``` +`logs` prints the log directory path, or a specific process's log contents: + +```sh +numux logs # Print log directory path +numux logs api # Print the api process log +``` + Set up completions for your shell: ```sh diff --git a/src/cli-flags.ts b/src/cli-flags.ts index ba308ce..7f57fbd 100644 --- a/src/cli-flags.ts +++ b/src/cli-flags.ts @@ -249,6 +249,20 @@ export const SUBCOMMANDS: SubcommandDef[] = [ return 'break' } }, + { + name: 'logs', + description: 'Open the log directory or a specific process log', + usage: 'logs [name]', + parse: (args, i, result) => { + result.logs = true + const next = args[i + 1] + if (next !== undefined && !next.startsWith('-')) { + result.logsProcess = next + i++ + } + return i + } + }, { name: 'completions', description: 'Generate shell completions (bash, zsh, fish)', diff --git a/src/cli.test.ts b/src/cli.test.ts index bf981e4..0402b97 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -261,6 +261,25 @@ describe('parseArgs', () => { expect(parseArgs(argv()).autoColors).toBe(false) }) + test('logs sets logs flag', () => { + const result = parseArgs(argv('logs')) + expect(result.logs).toBe(true) + expect(result.logsProcess).toBeUndefined() + }) + + test('logs with process name', () => { + const result = parseArgs(argv('logs', 'api')) + expect(result.logs).toBe(true) + expect(result.logsProcess).toBe('api') + }) + + test('logs with --log-dir flag', () => { + const result = parseArgs(argv('--log-dir', './my-logs', 'logs', 'web')) + expect(result.logs).toBe(true) + expect(result.logsProcess).toBe('web') + expect(result.logDir).toBe('./my-logs') + }) + test('exec parses process name and command', () => { const result = parseArgs(argv('exec', 'api', 'bunx', 'prisma', 'migrate')) expect(result.exec).toBe(true) diff --git a/src/cli.ts b/src/cli.ts index d0127e0..da042d2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -11,6 +11,8 @@ export interface ParsedArgs { exec: boolean execName?: string execCommand?: string + logs: boolean + logsProcess?: string completions?: string prefix: boolean killOthers: boolean @@ -51,6 +53,7 @@ export function parseArgs(argv: string[]): ParsedArgs { init: false, validate: false, exec: false, + logs: false, prefix: false, killOthers: false, killOthersOnFail: false, diff --git a/src/index.ts b/src/index.ts index 7729369..cafc313 100755 --- a/src/index.ts +++ b/src/index.ts @@ -76,6 +76,33 @@ async function main() { process.exit(0) } + if (parsed.logs) { + const logDir = parsed.logDir ?? (await resolveLogDir(parsed.configPath)) + const latestDir = resolve(logDir, 'latest') + const target = existsSync(latestDir) ? latestDir : logDir + + if (parsed.logsProcess) { + const logFile = resolve(target, `${parsed.logsProcess}.log`) + if (!existsSync(logFile)) { + const { readdirSync } = await import('node:fs') + const files = readdirSync(target) + .filter(f => f.endsWith('.log')) + .map(f => f.replace(/\.log$/, '')) + const available = files.length > 0 ? `Available: ${files.join(', ')}` : 'No log files found' + console.error(`No log file for "${parsed.logsProcess}". ${available}`) + process.exit(1) + } + const child = Bun.spawn(['cat', logFile], { + stdout: 'inherit', + stderr: 'inherit' + }) + process.exit(await child.exited) + } + + console.info(target) + process.exit(0) + } + if (parsed.validate) { const raw = expandWorkspaces(expandScriptPatterns(await loadConfig(parsed.configPath))) const warnings: ValidationWarning[] = [] @@ -266,6 +293,18 @@ function printWarnings(warnings: ValidationWarning[]): void { } } +async function resolveLogDir(configPath?: string): Promise { + try { + const raw = await loadConfig(configPath) + if (typeof raw.logDir === 'string' && raw.logDir.trim()) { + return resolve(raw.logDir.trim()) + } + } catch { + // Config may not exist — fall through to default + } + return defaultLogDir(process.cwd()) +} + main().catch(err => { console.error(err instanceof Error ? err.message : err) process.exit(1)