{"record":{"id":"66343353886433b2","repo":"agalwood/Motrix","slug":"plugin-manifest-invalid-663433","errorCode":"PLUGIN_MANIFEST_INVALID","errorMessage":"plugin.install.invalid_github_spec","messagePattern":"plugin\\.install\\.invalid_github_spec","errorType":"error_code","errorClass":"AppError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/install/source-resolver.ts","lineNumber":45,"sourceCode":"  | { type: 'volume'; containerPath: string }\n  | { type: 'env'; url: string }\n  | { type: 'builtin'; resourcePath: string }\n  // The registry — not the package host — is the trust anchor: the same\n  // plugin re-published from a different CDN keeps the same source URL, so\n  // upgrades via the registry never trip diff.sourceUrlChanged re-consent.\n  | { type: 'registry'; pluginId: string }\n\nexport interface NormalizedSource {\n  type: InstallSourceType\n  url: string\n}\n\nexport function normalizeSource(s: SourceInput): NormalizedSource {\n  switch (s.type) {\n    case 'github': {\n      const m = s.spec.match(/^([^/@]+)\\/([^/@]+)(?:@.+)?$/)\n      if (!m) {\n        throw new AppError(\n          ErrorCode.PluginManifestInvalid,\n          'plugin.install.invalid_github_spec'\n        )\n      }\n      const [, owner, repo] = m\n      return { type: 'github', url: `https://github.com/${owner}/${repo}` }\n    }\n    case 'url': {\n      let u: URL\n      try {\n        u = new URL(s.url)\n      } catch {\n        throw new AppError(\n          ErrorCode.PluginManifestInvalid,\n          'plugin.install.invalid_url'\n        )\n      }\n      if (u.protocol !== 'http:' && u.protocol !== 'https:') {","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/install/source-resolver.ts#L27-L63","documentation":"normalizeSource() rejects a github spec that does not match /^([^/@]+)\\/([^/@]+)(?:@.+)?$/. The regex requires exactly 'owner/repo' optionally followed by '@tag'. Any missing slash, extra '@', whitespace, or empty segment fails the match and throws before a URL is ever built.","triggerScenarios":"Passing {type:'github', spec:'owner'} (no slash), 'owner/repo@tag@extra', '/repo', 'owner/', or a spec with '@' in the owner segment.","commonSituations":"User typed 'github:owner' instead of 'github:owner/repo'; CLI arg parsing split 'owner/repo@v1' incorrectly; copy-paste included a leading 'github:' prefix into the spec field.","solutions":["Provide the spec in 'owner/repo' or 'owner/repo@tag' form.","Strip any 'github:' scheme prefix before passing the spec to normalizeSource.","Validate with the regex /^([^/@]+)\\/([^/@]+)(?:@.+)?$/ before calling."],"exampleFix":"// before\nnormalizeSource({type:'github', spec:'github:owner/repo@v1'})\n// after — strip scheme, keep owner/repo@tag\nnormalizeSource({type:'github', spec:'owner/repo@v1'})","handlingStrategy":"validation","validationCode":"function isValidGithubSpec(spec:string){\n  return /^([^/@]+)\\/([^/@]+)(?:@.+)?$/.test(spec)\n}\nif(!isValidGithubSpec(spec)) throw new Error('expected owner/repo[@tag]')\nnormalizeSource({type:'github', spec})","typeGuard":"const isGithubSpec = (s:string): boolean => /^([^/@]+)\\/([^/@]+)(?:@.+)?$/.test(s)","tryCatchPattern":"try { normalizeSource({type:'github', spec}) }\ncatch(e){ if(e instanceof AppError && e.message==='plugin.install.invalid_github_spec'){ /* prompt for owner/repo@tag */ } else throw e }","preventionTips":["Strip any 'github:' scheme prefix before passing the spec.","Validate the spec with the regex before calling normalizeSource.","Use a CLI flag parser that keeps 'owner/repo@tag' intact."],"tags":["plugin-install","source-resolver","github","validation","input-parsing"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}