paperclipai/paperclip · error · Error
Source must be a real directory
Error message
Source must be a real directory
What it means
prepareAnnouncementPublish resolves the given source directory and requires an on-disk directory; lstat is called and the result checked with isDirectory(). A nonexistent path (lstat throws ENOENT) or a path that is a file/symlink-to-file fails this check and throws 'Source must be a real directory'.
Solutions
- Verify the directory exists: `ls <dir>` from the same working directory the script runs in.
- Pass an absolute path or run the CLI from the repository root where 'announcements' lives.
- Point the argument at the directory containing current.json, not at current.json itself.
- Fix broken symlinks or case mismatches in the path.
Example fix
// before publish-announcements.ts ./announcments --publish // after publish-announcements.ts ./announcements --publish
Defensive patterns
Strategy: validation
Validate before calling
import { statSync } from 'node:fs';
const source = path.resolve(dir);
if (!statSync(source, { throwIfNoEntry: false })?.isDirectory()) {
throw new Error(`Source directory not found: ${source} (cwd=${process.cwd()})`);
} Type guard
const isDirectory = async (p) => { try { return (await lstat(p)).isDirectory(); } catch { return false; } }; Try / catch
try {
await prepareAnnouncementPublish(dir, staging, prefix);
} catch (e) {
if (String(e.message) === 'Source must be a real directory' || e.code === 'ENOENT') {
console.error(`Directory ${dir} does not exist; cwd=${process.cwd()}`);
process.exit(1);
}
throw e;
} Prevention
- Run the CLI from the repository root or pass absolute paths.
- ls the directory in CI before the publish step.
- Pass the directory containing current.json, not a file path.
- Watch for case-sensitivity mismatches on Linux CI runners.
When it happens
Trigger: Calling prepareAnnouncementPublish with a path that does not exist, points to a regular file (e.g. current.json itself), is a broken symlink, or is relative to the wrong working directory (common in CI where cwd differs).
Common situations: Running the publish CLI from a different working directory so the relative path 'announcements' does not exist; renaming the announcements folder; passing a file path instead of the directory; case-sensitive filesystem mismatch ('Announcements' vs 'announcements').
Related errors
- ACPX runtime directory must be a directory
- ACPX runtime directory must not be a filesystem root
- Assets must be a real directory
- Attempt source is not a real directory
- codex auth cache: account_id is not a valid account handle
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/1558184a113c9c01.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/publish-announcements.ts:44
if (arg === "--publish" || arg === "--dry-run") {
if (mode) throw new Error(usage);
mode = arg === "--publish" ? "publish" : "dry-run";
} else if (arg === "--staging") {
if (staging !== undefined || !args[index + 1]) throw new Error(usage);
staging = announcementIdSchema.parse(args[++index]);
} else if (arg.startsWith("--") || sourceDirectory !== undefined) {
throw new Error(usage);
} else {
sourceDirectory = arg;
}
}
return { sourceDirectory: sourceDirectory ?? (staging ? "announcements/examples/staging" : "announcements"), staging, publish: mode === "publish" };
}
export async function prepareAnnouncementPublish(sourceDirectory: string, staging?: string, hostPrefix?: string) {
const prefix = announcementPublishPrefix(staging, hostPrefix);
const source = path.resolve(sourceDirectory);
if (!(await lstat(source)).isDirectory()) throw new Error("Source must be a real directory");
const manifestPath = path.join(source, "current.json");
const stat = await lstat(manifestPath);
if (!stat.isFile() || stat.size > ANNOUNCEMENT_MANIFEST_MAX_BYTES) throw new Error("Invalid or oversized current.json");
const manifest = announcementManifestSchema.parse(JSON.parse(await readFile(manifestPath, "utf8")));
const files: Array<{ file: string; key: string; contentType: string; cacheControl: string }> = [];
for (const kind of ["image", "animation"] as const) {
const asset = manifest.announcement?.[kind];
if (!asset) continue;
if (!(await lstat(path.join(source, "assets"))).isDirectory()) throw new Error("Assets must be a real directory");
const assetPath = asset.path;
const file = path.join(source, assetPath);
const assetStat = await lstat(file);
const maximum = kind === "animation" ? ANNOUNCEMENT_ANIMATION_MAX_BYTES : ANNOUNCEMENT_IMAGE_MAX_BYTES;
if (!assetStat.isFile() || assetStat.size > maximum) throw new Error(`Invalid or oversized ${kind}`);
const bytes = await readFile(file);
const digest = createHash("sha256").update(bytes).digest("hex");
if (!assetPath.startsWith(`assets/${digest}.`)) throw new Error("Asset filename must match its SHA-256 digest");
if (kind === "animation") validateAnnouncementAnimation(bytes);View on GitHub (pinned to 3f1d897a7c)