{"record":{"id":"1558184a113c9c01","repo":"paperclipai/paperclip","slug":"source-must-be-a-real-directory","errorCode":null,"errorMessage":"Source must be a real directory","messagePattern":"Source must be a real directory","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/publish-announcements.ts","lineNumber":44,"sourceCode":"    if (arg === \"--publish\" || arg === \"--dry-run\") {\n      if (mode) throw new Error(usage);\n      mode = arg === \"--publish\" ? \"publish\" : \"dry-run\";\n    } else if (arg === \"--staging\") {\n      if (staging !== undefined || !args[index + 1]) throw new Error(usage);\n      staging = announcementIdSchema.parse(args[++index]);\n    } else if (arg.startsWith(\"--\") || sourceDirectory !== undefined) {\n      throw new Error(usage);\n    } else {\n      sourceDirectory = arg;\n    }\n  }\n  return { sourceDirectory: sourceDirectory ?? (staging ? \"announcements/examples/staging\" : \"announcements\"), staging, publish: mode === \"publish\" };\n}\n\nexport async function prepareAnnouncementPublish(sourceDirectory: string, staging?: string, hostPrefix?: string) {\n  const prefix = announcementPublishPrefix(staging, hostPrefix);\n  const source = path.resolve(sourceDirectory);\n  if (!(await lstat(source)).isDirectory()) throw new Error(\"Source must be a real directory\");\n  const manifestPath = path.join(source, \"current.json\");\n  const stat = await lstat(manifestPath);\n  if (!stat.isFile() || stat.size > ANNOUNCEMENT_MANIFEST_MAX_BYTES) throw new Error(\"Invalid or oversized current.json\");\n  const manifest = announcementManifestSchema.parse(JSON.parse(await readFile(manifestPath, \"utf8\")));\n  const files: Array<{ file: string; key: string; contentType: string; cacheControl: string }> = [];\n  for (const kind of [\"image\", \"animation\"] as const) {\n    const asset = manifest.announcement?.[kind];\n    if (!asset) continue;\n    if (!(await lstat(path.join(source, \"assets\"))).isDirectory()) throw new Error(\"Assets must be a real directory\");\n    const assetPath = asset.path;\n    const file = path.join(source, assetPath);\n    const assetStat = await lstat(file);\n    const maximum = kind === \"animation\" ? ANNOUNCEMENT_ANIMATION_MAX_BYTES : ANNOUNCEMENT_IMAGE_MAX_BYTES;\n    if (!assetStat.isFile() || assetStat.size > maximum) throw new Error(`Invalid or oversized ${kind}`);\n    const bytes = await readFile(file);\n    const digest = createHash(\"sha256\").update(bytes).digest(\"hex\");\n    if (!assetPath.startsWith(`assets/${digest}.`)) throw new Error(\"Asset filename must match its SHA-256 digest\");\n    if (kind === \"animation\") validateAnnouncementAnimation(bytes);","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/scripts/publish-announcements.ts#L26-L62","documentation":"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'.","triggerScenarios":"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).","commonSituations":"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').","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."],"exampleFix":"// before\npublish-announcements.ts ./announcments --publish\n// after\npublish-announcements.ts ./announcements --publish","handlingStrategy":"validation","validationCode":"import { statSync } from 'node:fs';\nconst source = path.resolve(dir);\nif (!statSync(source, { throwIfNoEntry: false })?.isDirectory()) {\n  throw new Error(`Source directory not found: ${source} (cwd=${process.cwd()})`);\n}","typeGuard":"const isDirectory = async (p) => { try { return (await lstat(p)).isDirectory(); } catch { return false; } };","tryCatchPattern":"try {\n  await prepareAnnouncementPublish(dir, staging, prefix);\n} catch (e) {\n  if (String(e.message) === 'Source must be a real directory' || e.code === 'ENOENT') {\n    console.error(`Directory ${dir} does not exist; cwd=${process.cwd()}`);\n    process.exit(1);\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["filesystem","path","validation"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}