agalwood/Motrix · error · AppError

PLUGIN_MANIFEST_INVALID

PLUGIN_MANIFEST_INVALID

Error message

plugin.install.invalid_github_spec

What it means

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.

Source

Thrown at src/core/plugin/install/source-resolver.ts:45

  | { type: 'volume'; containerPath: string }
  | { type: 'env'; url: string }
  | { type: 'builtin'; resourcePath: string }
  // The registry — not the package host — is the trust anchor: the same
  // plugin re-published from a different CDN keeps the same source URL, so
  // upgrades via the registry never trip diff.sourceUrlChanged re-consent.
  | { type: 'registry'; pluginId: string }

export interface NormalizedSource {
  type: InstallSourceType
  url: string
}

export function normalizeSource(s: SourceInput): NormalizedSource {
  switch (s.type) {
    case 'github': {
      const m = s.spec.match(/^([^/@]+)\/([^/@]+)(?:@.+)?$/)
      if (!m) {
        throw new AppError(
          ErrorCode.PluginManifestInvalid,
          'plugin.install.invalid_github_spec'
        )
      }
      const [, owner, repo] = m
      return { type: 'github', url: `https://github.com/${owner}/${repo}` }
    }
    case 'url': {
      let u: URL
      try {
        u = new URL(s.url)
      } catch {
        throw new AppError(
          ErrorCode.PluginManifestInvalid,
          'plugin.install.invalid_url'
        )
      }
      if (u.protocol !== 'http:' && u.protocol !== 'https:') {

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Provide the spec in 'owner/repo' or 'owner/repo@tag' form.
  2. Strip any 'github:' scheme prefix before passing the spec to normalizeSource.
  3. Validate with the regex /^([^/@]+)\/([^/@]+)(?:@.+)?$/ before calling.

Example fix

// before
normalizeSource({type:'github', spec:'github:owner/repo@v1'})
// after — strip scheme, keep owner/repo@tag
normalizeSource({type:'github', spec:'owner/repo@v1'})
Defensive patterns

Strategy: validation

Validate before calling

function isValidGithubSpec(spec:string){
  return /^([^/@]+)\/([^/@]+)(?:@.+)?$/.test(spec)
}
if(!isValidGithubSpec(spec)) throw new Error('expected owner/repo[@tag]')
normalizeSource({type:'github', spec})

Type guard

const isGithubSpec = (s:string): boolean => /^([^/@]+)\/([^/@]+)(?:@.+)?$/.test(s)

Try / catch

try { normalizeSource({type:'github', spec}) }
catch(e){ if(e instanceof AppError && e.message==='plugin.install.invalid_github_spec'){ /* prompt for owner/repo@tag */ } else throw e }

Prevention

When it happens

Trigger: Passing {type:'github', spec:'owner'} (no slash), 'owner/repo@tag@extra', '/repo', 'owner/', or a spec with '@' in the owner segment.

Common situations: 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.

Related errors


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/66343353886433b2. Report an issue: GitHub.