jackwener/OpenCLI · error · Error

not a directory

Error message

not a directory

What it means

reserveAudioFile creates the output directory, then verifies it with statSync and asserts it is actually a directory before reserving a target file with a lock file. If the path exists but is not a directory (e.g. a regular file or symlink to a file), it throws Error('not a directory'), which the catch block re-wraps into CommandExecutionError('MiniMax music cannot reserve output file <target>: not a directory').

Source

Thrown at clis/minimax/utils.js:186

}

export function resolveOutputDir(value) {
    const raw = String(value ?? '').trim();
    if (!raw) return path.join(os.homedir(), 'Music', 'minimax');
    if (raw === '~') return os.homedir();
    if (raw.startsWith('~/')) return path.join(os.homedir(), raw.slice(2));
    if (raw.startsWith('~')) throw new ArgumentError(`Unsupported home-directory path: ${raw}`);
    return path.resolve(raw);
}

export function reserveAudioFile(dir, model, format, now = new Date()) {
    const stamp = now.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
    const target = path.join(dir, `${model}-${stamp}.${format}`);
    const lock = `${target}.lock`;
    const staging = `${target}.${process.pid}.${randomUUID()}.tmp`;
    try {
        fs.mkdirSync(dir, { recursive: true });
        if (!fs.statSync(dir).isDirectory()) throw new Error('not a directory');
        fs.accessSync(dir, fs.constants.W_OK);
        if (fs.existsSync(target)) throw new Error('target already exists');
        fs.writeFileSync(lock, String(process.pid), { flag: 'wx', mode: 0o600 });
        if (fs.existsSync(target)) {
            fs.rmSync(lock, { force: true });
            throw new Error('target appeared during reservation');
        }
        return { target, lock, staging };
    } catch (error) {
        throw new CommandExecutionError(`MiniMax music cannot reserve output file ${target}: ${error?.message ?? error}`);
    }
}

export function commitAudioFile(reservation, bytes) {
    try {
        fs.writeFileSync(reservation.staging, bytes, { flag: 'wx', mode: 0o600 });
        // A same-filesystem hard link publishes the complete staging inode and
        // fails if target already exists on POSIX and Windows alike.

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Inspect the path: run ls -la on the configured dir; if it is a file, remove or rename it (rm <path>) and re-run
  2. Point the output-dir config at a genuine directory path
  3. Check for symlinks: replace a symlink-to-file with a real directory (mkdir <path>)
  4. Verify permissions/mount state if the path is on a network or removable volume

Example fix

// before: 'minimax' exists as a regular file
reserveAudioFile('~/Music/minimax', model, 'wav'); // throws
// after (shell)
// rm ~/Music/minimax && mkdir -p ~/Music/minimax
reserveAudioFile('~/Music/minimax', model, 'wav');
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from 'node:fs';
function assertUsableDir(dir) {
    try {
        const st = fs.statSync(dir);
        if (!st.isDirectory()) throw new Error(`${dir} exists but is not a directory`);
    } catch (e) {
        if (e.code === 'ENOENT') return; // will be created
        if (String(e.message).includes('not a directory')) throw e;
        throw e;
    }
}
// run assertUsableDir(dir) before reserveAudioFile

Type guard

function isDirectoryPath(p) {
    try { return fs.statSync(p).isDirectory(); } catch { return false; }
}

Try / catch

try {
    const r = reserveAudioFile(dir, model, format);
} catch (e) {
    if (String(e.message).includes('not a directory')) {
        fs.rmSync(dir, { force: true });          // remove stray file
        fs.mkdirSync(dir, { recursive: true });   // recreate as directory
        // then retry reserveAudioFile
    } else throw e;
}

Prevention

When it happens

Trigger: Passing a dir argument that exists on disk as a regular file/symlink-to-file; or a path component where mkdirSync created something but statSync shows a non-directory inode.

Common situations: Config points the output dir at an existing file (e.g. a stale 'minimax' file left by a previous tool); a mount point replaced by a file; a broken symlink where a directory was expected.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/91c9da6ef01d66b3. Report an issue: GitHub.