{"record":{"id":"e8c3a3c64bbfea18","repo":"agalwood/Motrix","slug":"plugin-manifest-invalid-e8c3a3","errorCode":"PLUGIN_MANIFEST_INVALID","errorMessage":"plugin.install.invalid_github_spec","messagePattern":"plugin\\.install\\.invalid_github_spec","errorType":"exception","errorClass":"AppError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/install/github-fetcher.ts","lineNumber":21,"sourceCode":"//\n// We don't read the manifest here — that happens later in `extractMoext`.\n// We just provide a clean tarball-style \"bring me the bytes\" boundary.\n\nimport { mkdir, rm, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport { AppError, ErrorCode } from '@shared/errors'\nimport { Agent, interceptors, request } from 'undici'\n\nexport interface GhReleaseSpec {\n  owner: string\n  repo: string\n  tag?: string\n}\n\nexport function parseGithubSpec(spec: string): GhReleaseSpec {\n  const m = spec.match(/^([^/@\\s]+)\\/([^/@\\s]+)(?:@(.+))?$/)\n  if (!m) {\n    throw new AppError(\n      ErrorCode.PluginManifestInvalid,\n      'plugin.install.invalid_github_spec'\n    )\n  }\n  const [, owner, repo, tag] = m\n  return tag ? { owner, repo, tag } : { owner, repo }\n}\n\ninterface GhReleaseResponse {\n  tag_name: string\n  assets: Array<{ name: string; browser_download_url: string }>\n}\n\nexport interface DownloadResult {\n  tag: string\n  assetName: string\n}\n","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/install/github-fetcher.ts#L3-L39","documentation":"parseGithubSpec requires the form owner/repo or owner/repo@tag (regex ^([^/@\\s]+)/([^/@\\s]+)(?:@(.+))?$). A spec with whitespace, a missing slash, a leading @, extra path segments, or an unstripped 'github:' prefix does not match and throws PLUGIN_MANIFEST_INVALID.","triggerScenarios":"Caller passes 'foo' (no slash), 'foo/bar/baz', ' owner/repo' (leading space), '@owner/repo', or 'github:owner/repo' without stripping the prefix.","commonSituations":"User pastes a full GitHub URL or the 'github:' shorthand into the install field; whitespace from copy-paste; extra trailing slash or path.","solutions":["Pass the spec as owner/repo or owner/repo@tag with no prefix and no whitespace.","Strip a leading 'github:' and trim whitespace before calling parseGithubSpec.","Validate the spec with the same regex before submitting it to the installer."],"exampleFix":"// before\nparseGithubSpec('github:owner/repo@v1.0') // no match\n// after\nconst spec = raw.replace(/^github:/, '').trim()\nparseGithubSpec(spec) // 'owner/repo@v1.0'","handlingStrategy":"validation","validationCode":"const GITHUB_SPEC_RE = /^([^/@\\s]+)\\/([^/@\\s]+)(?:@(.+))?$/\nfunction normalizeGithubSpec(raw: string): string {\n  const spec = raw.replace(/^github:/, '').trim()\n  if (!GITHUB_SPEC_RE.test(spec)) {\n    throw new Error(`invalid github spec: ${raw} (expected owner/repo[@tag])`)\n  }\n  return spec\n}","typeGuard":"function isValidGithubSpec(raw: string): boolean {\n  return /^([^/@\\s]+)\\/([^/@\\s]+)(?:@(.+))?$/.test(raw.replace(/^github:/, '').trim())\n}","tryCatchPattern":"try {\n  parseGithubSpec(spec)\n} catch (e) {\n  if (e.code === 'PLUGIN_MANIFEST_INVALID' && /invalid_github_spec/.test(e.message)) {\n    // prompt the user for owner/repo[@tag]; strip 'github:' and whitespace before retrying\n  } else throw e\n}","preventionTips":["Strip a leading 'github:' and trim whitespace before parsing.","Validate with the owner/repo[@tag] regex before submitting to the installer.","Reject full URLs and multi-segment paths at the input boundary."],"tags":["plugin","install","github","validation"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}