diff --git a/src/index.ts b/src/index.ts index 8a268e9..7729369 100755 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import { type Color, colorFromName } from './utils/color' import { loadEnvFiles } from './utils/env-file' import { LogWriter } from './utils/log-writer' import { enableDebugLog } from './utils/logger' +import { defaultLogDir } from './utils/project-name' import { setupShutdownHandlers } from './utils/shutdown' const HELP = generateHelp() @@ -235,8 +236,8 @@ async function main() { const manager = new ProcessManager(config) - const logDir = parsed.logDir ?? config.logDir - const logWriter = logDir ? LogWriter.createPersistent(logDir) : LogWriter.createTemp() + const logDir = parsed.logDir ?? config.logDir ?? defaultLogDir(process.cwd()) + const logWriter = LogWriter.createPersistent(logDir) printWarnings(warnings) diff --git a/src/utils/project-name.test.ts b/src/utils/project-name.test.ts new file mode 100644 index 0000000..f1ea533 --- /dev/null +++ b/src/utils/project-name.test.ts @@ -0,0 +1,65 @@ +import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import { mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { defaultLogDir, resolveProjectName } from './project-name' + +describe('resolveProjectName', () => { + let dir: string + + beforeEach(() => { + dir = join(tmpdir(), `numux-test-${Date.now()}-${Math.random().toString(36).slice(2)}`) + mkdirSync(dir, { recursive: true }) + }) + + afterEach(() => { + rmSync(dir, { recursive: true }) + }) + + test('reads name from package.json', () => { + writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'my-app' })) + expect(resolveProjectName(dir)).toBe('my-app') + }) + + test('strips npm scope from name', () => { + writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: '@org/my-app' })) + expect(resolveProjectName(dir)).toBe('my-app') + }) + + test('falls back to directory basename without package.json', () => { + const sub = join(dir, 'my-project') + mkdirSync(sub) + expect(resolveProjectName(sub)).toBe('my-project') + }) + + test('falls back to directory basename when name is empty', () => { + const sub = join(dir, 'fallback-dir') + mkdirSync(sub) + writeFileSync(join(sub, 'package.json'), JSON.stringify({ name: '' })) + expect(resolveProjectName(sub)).toBe('fallback-dir') + }) + + test('falls back to directory basename when name is missing', () => { + const sub = join(dir, 'no-name') + mkdirSync(sub) + writeFileSync(join(sub, 'package.json'), JSON.stringify({ version: '1.0.0' })) + expect(resolveProjectName(sub)).toBe('no-name') + }) + + test('falls back to directory basename on invalid JSON', () => { + const sub = join(dir, 'bad-json') + mkdirSync(sub) + writeFileSync(join(sub, 'package.json'), 'not json') + expect(resolveProjectName(sub)).toBe('bad-json') + }) +}) + +describe('defaultLogDir', () => { + test('returns /tmp/numux/', () => { + const dir = join(tmpdir(), `numux-test-${Date.now()}`) + mkdirSync(dir, { recursive: true }) + writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'cool-app' })) + expect(defaultLogDir(dir)).toBe(join(tmpdir(), 'numux', 'cool-app')) + rmSync(dir, { recursive: true }) + }) +}) diff --git a/src/utils/project-name.ts b/src/utils/project-name.ts new file mode 100644 index 0000000..558ad65 --- /dev/null +++ b/src/utils/project-name.ts @@ -0,0 +1,25 @@ +import { existsSync, readFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { basename, join } from 'node:path' + +/** Resolve project name from package.json name (scope-stripped) or directory basename. */ +export function resolveProjectName(cwd: string): string { + try { + const pkgPath = join(cwd, 'package.json') + if (existsSync(pkgPath)) { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) + if (typeof pkg.name === 'string' && pkg.name.trim()) { + // Strip npm scope (e.g. @org/name -> name) + return pkg.name.replace(/^@[^/]+\//, '').trim() + } + } + } catch { + // Fall through to directory name + } + return basename(cwd) +} + +/** Default log directory: /tmp/numux/ */ +export function defaultLogDir(cwd: string): string { + return join(tmpdir(), 'numux', resolveProjectName(cwd)) +}