{"record":{"id":"19cbdaa3e0c8e5f7","repo":"jackwener/OpenCLI","slug":"output-must-be-a-directory-path","errorCode":null,"errorMessage":"output must be a directory path","messagePattern":"output must be a directory path","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/pixiv/novel-download-utils.js","lineNumber":60,"sourceCode":"    wordCount,\n    bookmarkCount,\n  };\n}\n\nexport function normalizeNovelFileFormat(value) {\n  if (value !== undefined && typeof value !== 'string') {\n    throw new ArgumentError('Novel download format must be txt or md');\n  }\n  const format = (value ?? 'txt').toLowerCase();\n  if (format !== 'txt' && format !== 'md') {\n    throw new ArgumentError(`Unsupported novel download format: ${format}. Supported formats: txt, md.`);\n  }\n  return format;\n}\n\nexport function normalizePixivOutputRoot(value, fallback) {\n  if (value !== undefined && typeof value !== 'string') {\n    throw new ArgumentError('output must be a directory path');\n  }\n  const raw = value ?? fallback;\n  if (!raw || raw.includes('\\0')) {\n    throw new ArgumentError('output must be a non-empty directory path');\n  }\n  const resolved = path.resolve(raw);\n  let ancestor = resolved;\n  const missingParts = [];\n  let ancestorStat;\n  while (!ancestorStat) {\n    try {\n      ancestorStat = fs.lstatSync(ancestor);\n    } catch (error) {\n      if (error?.code !== 'ENOENT') {\n        throw new ArgumentError(`output path is not a safe directory: ${ancestor}`);\n      }\n      const parent = path.dirname(ancestor);\n      if (parent === ancestor) {","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/pixiv/novel-download-utils.js#L42-L78","documentation":"normalizePixivOutputRoot validates the --output option before any download is written. It throws ArgumentError('output must be a directory path') when the output value is supplied but is not a string (e.g. a number, boolean, or object was passed). The library only accepts a directory path string for output.","triggerScenarios":"Calling normalizePixivOutputRoot (directly or via outputRoot/outputDir/output) with a defined non-string value such as normalizePixivOutputRoot(123), normalizePixivOutputRoot(true), or a parsed config object where a string path was expected.","commonSituations":"Programmatic use of the CLI API passing a numeric or boolean option; YAML/JSON config that parses --output: 2024 (a number) instead of quoting it; a script variable holding null-coalesced value of wrong type.","solutions":["Pass a string directory path, e.g. output: './pixiv-downloads/novels'.","Quote numeric-looking paths in config files (output: \"2024\") so YAML/JSON does not coerce them to numbers.","Coerce with String(value) before calling, or omit the argument entirely to use the built-in fallback ('./pixiv-downloads/novels')."],"exampleFix":"// before\nawait downloadNovel(id, { output: 2024 });\n// after\nawait downloadNovel(id, { output: String(config.output ?? './pixiv-downloads/novels') });","handlingStrategy":"type-guard","validationCode":"if (output !== undefined && typeof output !== 'string') {\n  throw new TypeError('output must be a string directory path');\n}","typeGuard":"function isDirectoryPath(v) {\n  return v === undefined || (typeof v === 'string' && v.length > 0);\n}","tryCatchPattern":"try {\n  await downloadNovel(id, { output });\n} catch (e) {\n  if (e.name === 'ArgumentError' && /output must be a directory path/.test(e.message)) {\n    output = String(output);\n  } else throw e;\n}","preventionTips":["Quote numeric-looking path values in YAML/JSON config files.","Coerce CLI/env inputs with String() before passing path options.","Type-check option objects with a schema (zod/ajv) at the CLI boundary."],"tags":["argument-validation","type-error","filesystem","path"],"backgroundTag":"invalid-argument-type","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}