Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ numux
numux init # Create a starter numux.config.ts
numux validate # Validate config and show process dependency graph
numux exec <name> [--] <command> # Run a command in a process's environment
numux logs [name] # Open log directory or view a process log
numux completions <shell> # Generate shell completions (bash, zsh, fish)
```

Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/cli-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down
19 changes: 19 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface ParsedArgs {
exec: boolean
execName?: string
execCommand?: string
logs: boolean
logsProcess?: string
completions?: string
prefix: boolean
killOthers: boolean
Expand Down Expand Up @@ -51,6 +53,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
init: false,
validate: false,
exec: false,
logs: false,
prefix: false,
killOthers: false,
killOthersOnFail: false,
Expand Down
39 changes: 39 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = []
Expand Down Expand Up @@ -266,6 +293,18 @@ function printWarnings(warnings: ValidationWarning[]): void {
}
}

async function resolveLogDir(configPath?: string): Promise<string> {
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)
Expand Down
Loading