can1357/oh-my-pi · error

Not a file: ${shortenPath(resolved)}

Error message

Not a file: ${shortenPath(resolved)}

What it means

resolveCompressTargets resolves non-glob arguments to literal paths via path.resolve + fs.stat. If the path does not exist or is not a regular file (directory, socket, symlink-to-dir), it throws with the shortened path. Only plain files can go through the compress rewrite/approve loop.

Source

Thrown at packages/coding-agent/src/compress/index.ts:70

 * fewer files than asked is worse than failing.
 */
export async function resolveCompressTargets(patterns: readonly string[], cwd: string): Promise<string[]> {
	const found = new Set<string>();
	for (const pattern of patterns) {
		if (/[*?[\]{}]/.test(pattern)) {
			// `dot: true` — prompt corpora live under dot directories such as `.omp/commands`.
			const matches = new Bun.Glob(pattern).scanSync({ cwd, absolute: true, onlyFiles: true, dot: true });
			let matched = 0;
			for (const match of matches) {
				found.add(match);
				matched += 1;
			}
			if (matched === 0) throw new Error(`No files matched "${pattern}"`);
			continue;
		}
		const resolved = path.resolve(cwd, pattern);
		const stat = await fs.stat(resolved).catch(() => undefined);
		if (!stat?.isFile()) throw new Error(`Not a file: ${shortenPath(resolved)}`);
		found.add(resolved);
	}
	return [...found].sort();
}

/** Compress every requested file through the rewrite/approve loop. */
export async function runCompressCommand(options: CompressCommandOptions): Promise<CompressResult> {
	const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
	const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
	if (!Number.isInteger(maxRounds) || maxRounds <= 0) throw new Error("--rounds must be a positive integer");
	if (!Number.isInteger(concurrency) || concurrency <= 0) throw new Error("--agents must be a positive integer");
	if (options.inPlace && options.output) throw new Error("--in-place and --out are mutually exclusive");
	// Paths and patterns follow the shell's cwd, as a file-taking CLI must; the project
	// dir only scopes settings discovery for the sessions.
	const invocationDir = process.cwd();
	const cwd = getProjectDir();
	const targets = await resolveCompressTargets(options.files, invocationDir);
	if (targets.length === 0) throw new Error("No files to compress");

View on GitHub (pinned to 9690622007)

Solutions

  1. Verify the path exists with `ls <path>` and that it is a file, not a directory.
  2. cd to the intended directory or pass an absolute path (paths resolve against the shell cwd).
  3. To compress many files use a glob plus --in-place instead of a directory.
  4. Fix typos or re-create the missing file.

Example fix

// before
omp compress src/        // Not a file: src
// after
omp compress 'src/**/*.ts' --in-place
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from "node:fs/promises";
async function isFileArg(p: string): Promise<boolean> {
  try { return (await fs.stat(p)).isFile(); } catch { return false; }
}
// filter args: for (const a of args) if (!(await isFileArg(a))) console.error(`skipping ${a}`);

Try / catch

try {
  await runCompressCommand({ files: [target] });
} catch (err) {
  if (err instanceof Error && err.message.startsWith("Not a file:")) {
    process.stderr.write(`${err.message} — pass a regular file, not a directory\n`);
  } else throw err;
}

Prevention

When it happens

Trigger: Passing a directory (`omp compress src/`), a nonexistent path, a path that exists relative to a different cwd than expected, or a special file; stat fails (ENOENT/permission) and `stat?.isFile()` is falsy.

Common situations: Passing a directory expecting recursive compression; typo in filename; running from the wrong directory so relative path resolves elsewhere; deleted/renamed file since last command; permission-denied path silently treated as missing.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/59f2733f0df2aab0. Report an issue: GitHub.