{"record":{"id":"3c28b17ebf021dcf","repo":"jackwener/OpenCLI","slug":"twitter-likes-resume-file-filepath-has-an-inval","errorCode":null,"errorMessage":"Twitter likes resume file ${filePath} has an invalid shape","messagePattern":"Twitter likes resume file (.+?) has an invalid shape","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/likes.js","lineNumber":137,"sourceCode":"}\nfunction readResumeFile(filePath, expected = null) {\n    if (!filePath || !fs.existsSync(filePath))\n        return null;\n    let parsed;\n    try {\n        parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));\n    }\n    catch (error) {\n        throw new CommandExecutionError(`Could not parse Twitter likes resume file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);\n    }\n    const count = parsed?.count;\n    const cursor = parsed?.cursor == null ? null : String(parsed.cursor);\n    const outputFile = parsed?.outputFile ? path.resolve(String(parsed.outputFile)) : null;\n    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)\n        || !Number.isInteger(count) || count < 0\n        || (parsed.cursor != null && typeof parsed.cursor !== 'string')\n        || (cursor !== null && !cursor.trim())) {\n        throw new CommandExecutionError(`Twitter likes resume file ${filePath} has an invalid shape`);\n    }\n    if (expected) {\n        if (parsed.source !== expected.source)\n            throw new ArgumentError(`Resume file source mismatch: expected ${expected.source}, found ${parsed.source || 'unknown'}`);\n        if (String(parsed.username || '').toLowerCase() !== String(expected.username).toLowerCase())\n            throw new ArgumentError(`Resume file username mismatch: expected @${expected.username}, found @${parsed.username || 'unknown'}`);\n        if (outputFile !== expected.outputFile)\n            throw new ArgumentError(`Resume file output mismatch: expected ${expected.outputFile || 'in-memory mode'}, found ${outputFile || 'in-memory mode'}`);\n        if (!expected.outputFile && !Array.isArray(parsed.tweets))\n            throw new CommandExecutionError(`Twitter likes resume file ${filePath} is missing in-memory tweets`);\n        if (!expected.outputFile && parsed.tweets.length !== count)\n            throw new CommandExecutionError(`Twitter likes resume file ${filePath} count does not match its in-memory tweets`);\n        if (parsed.complete)\n            throw new CommandExecutionError(`Twitter likes resume file ${filePath} is already marked complete`);\n    }\n    return {\n        cursor,\n        count,","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/likes.js#L119-L155","documentation":"readResumeFile validates the parsed resume file's structure and throws this CommandExecutionError when it parses as JSON but does not match the expected schema: it must be a non-array object with an integer count >= 0, an optional string cursor, and (if present) a cursor that is non-blank. This guards against resuming from a semantically invalid file.","triggerScenarios":"Resuming `twitter likes` with a file whose parsed JSON lacks required/valid fields — e.g. count is missing, non-integer, or negative; cursor is present but not a string; cursor is an empty/whitespace string; the JSON root is an array or null.","commonSituations":"Hand-edited resume files with wrong types (count as string \"42\"); a resume file generated by a different tool or library version with a different schema; accidentally pointing --resume at an unrelated JSON file (like a package.json or output data file).","solutions":["Compare the file against the expected schema: {count: integer>=0, cursor?: string, source, username, outputFile?, tweets?} and fix the fields","Delete the invalid file and start the likes run fresh, letting the library write a new resume file","Ensure you are pointing --resume at the library-generated resume file, not another JSON artifact","Check for library version mismatch — regenerate the resume with the same version that will resume it"],"exampleFix":"// before (invalid shape)\n{\"count\": \"42\", \"cursor\": \"\"}\n// after\n{\"count\": 42, \"cursor\": \"abc123cursor\", \"source\": \"likes\", \"username\": \"someuser\"}","handlingStrategy":"validation","validationCode":"function isValidResumeShape(data) {\n  return data && typeof data === 'object' && !Array.isArray(data)\n    && Number.isInteger(data.count) && data.count >= 0\n    && (data.cursor == null || (typeof data.cursor === 'string' && data.cursor.trim() !== ''));\n}\nconst parsed = JSON.parse(fs.readFileSync(resumePath, 'utf8'));\nif (!isValidResumeShape(parsed)) throw new Error('Invalid resume shape before resuming');","typeGuard":"function isResumeFile(v) {\n  return typeof v === 'object' && v !== null && !Array.isArray(v)\n    && Number.isInteger(v.count) && v.count >= 0\n    && (v.cursor == null || typeof v.cursor === 'string');\n}","tryCatchPattern":"try {\n  await runLikes({ resume: resumePath });\n} catch (e) {\n  if (/has an invalid shape/.test(e.message)) {\n    fs.rmSync(resumePath);\n    return runLikes({}); // restart fresh\n  }\n  throw e;\n}","preventionTips":["Never hand-edit resume files; regenerate them via the library","Point --resume only at files the library itself wrote","Keep the same library version for the run that writes and the run that resumes","Validate shape (count integer, cursor string) before resuming"],"tags":["twitter","resume-file","schema-validation","validation"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}