jdx/mise · error

mode symlink-each requires the source to be a directory: {}

Error message

mode symlink-each requires the source to be a directory: {}

What it means

`mode = "symlink-each"` links each entry inside the source directory individually into the target directory, so the source must be a directory. `check_symlink_each` validates this before computing stale links and fails with the offending source path; callers wrap the error with the entry's status row or apply-error context.

Source

Thrown at src/system/files.rs:1374

        }
    } else if target.exists() {
        Ok(FileState::Differs("exists but is not a symlink".into()))
    } else {
        Ok(FileState::Missing)
    }
}

fn points_at_same_file(target: &Path, source: &Path) -> bool {
    match (target.canonicalize(), source.canonicalize()) {
        (Ok(a), Ok(b)) => a == b,
        _ => false,
    }
}

fn check_symlink_each(req: &FileRequest) -> Result<FileState> {
    if !req.source.is_dir() {
        // callers add the entry's context (status row / apply error list)
        bail!(
            "mode symlink-each requires the source to be a directory: {}",
            req.source.display_user()
        );
    }
    let stale = stale_links(req)?;
    let stale_reason = || format!("{} stale link(s)", stale.len());
    let files = walk_source_files(req)?;
    // with no files to link the desired state is just the target directory —
    // a blocking non-directory must still surface (and be --force-able)
    if files.is_empty() {
        return if req.target.is_dir() {
            if stale.is_empty() {
                Ok(FileState::Applied)
            } else {
                Ok(FileState::Differs(stale_reason()))
            }
        } else if req.target.exists() || req.target.is_symlink() {
            Ok(FileState::Differs("exists but is not a directory".into()))

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Point `source` at the directory you want mirrored (e.g. `source = "config/app"` not `config/app/app.conf`).
  2. If the source is genuinely a single file, drop `symlink-each` and use the default symlink or copy mode.
  3. Create the missing source directory in the dotfiles root, or fix the target so the implied source resolves to an existing directory.

Example fix

// before
[dotfiles."~/.config/app"]
mode = "symlink-each"
source = "app/app.conf"   # a file

// after
[dotfiles."~/.config/app"]
mode = "symlink-each"
source = "app"            # the directory
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
function assertSourceIsDir(sourcePath) {
  if (!fs.statSync(sourcePath).isDirectory())
    throw new Error(`mode symlink-each requires the source to be a directory: ${sourcePath}`);
}

Try / catch

try {
  applyDotfiles();
} catch (e) {
  if (/symlink-each requires the source to be a directory/.test(e.message)) {
    console.error('Point source at a directory, or use plain symlink/copy mode for a single file');
  } else throw e;
}

Prevention

When it happens

Trigger: Declaring `mode = "symlink-each"` for a dotfile whose `source` (or implied source) resolves to a regular file or does not exist when `mise dotfiles` status/apply runs `check_symlink_each`.

Common situations: Pointing symlink-each at a single file (meant plain symlink mode), the source file missing after renaming something in the dotfiles repo, or an implied source resolving to the wrong path because the target nesting differs from the repo layout.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/e303b82793bf145e. Report an issue: GitHub.