{"record":{"id":"b1fbdb58b37deb48","repo":"Hmbown/CodeWhale","slug":"installed-plugin-has-no-supported-manifest","errorCode":null,"errorMessage":"installed plugin has no supported manifest","messagePattern":"installed plugin has no supported manifest","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/plugins/install/place.rs","lineNumber":91,"sourceCode":"    // Write the marker last so a partial install never leaves a stale\n    // `.installed-from` on disk.\n    if let Err(error) = skill_install::write_installed_from_v2(\n        &final_path,\n        spec,\n        url,\n        source_checksum,\n        &staged.content_hash,\n        &staged.name,\n    ) {\n        let _ = fs::remove_dir_all(&final_path);\n        if let Some(backup) = backup_path.take() {\n            let _ = fs::rename(&backup, &final_path);\n        }\n        return Err(error);\n    }\n    let installed_content_hash =\n        match crate::plugins::agent_plugin::resolve_manifest_path(&final_path)\n            .ok_or_else(|| anyhow::anyhow!(\"installed plugin has no supported manifest\"))\n            .and_then(|manifest_path| {\n                crate::plugins::manifest::PluginManifest::validate_from_path(&manifest_path)\n                    .map(|validated| validated.content_hash)\n                    .map_err(anyhow::Error::msg)\n            }) {\n            Ok(hash) => hash,\n            Err(error) => {\n                let _ = fs::remove_dir_all(&final_path);\n                if let Some(backup) = backup_path.take() {\n                    let _ = fs::rename(&backup, &final_path);\n                }\n                return Err(error).context(\"installed plugin failed post-copy validation\");\n            }\n        };\n    if let Some(backup) = backup_path {\n        fs::remove_dir_all(&backup).ok();\n    }\n","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/plugins/install/place.rs#L73-L109","documentation":"After finalizing a plugin install by moving files into place, the installed tree must contain a recognizable plugin manifest (plugin.json, .claude-plugin/plugin.json, kimi.plugin.json, or plugin.toml). `resolve_manifest_path` returned None, so the installed directory is not a supported plugin. The install is aborted and rolled back (restoring any backup).","triggerScenarios":"`finalize_install` (called from `install_inner` and `install_remote_bytes`) calls `resolve_manifest_path(&final_path)` on the just-installed tree and it finds no supported manifest file.","commonSituations":"Installing a tarball or repo whose manifest is nested deeper than expected, a manifest that was deleted or renamed during packaging, downloading a non-plugin release archive, or a manifest format not in the supported set.","solutions":["Verify the bundle contains a supported manifest at its root or under .claude-plugin/ (plugin.json, kimi.plugin.json, or plugin.toml).","Re-download or re-export the plugin archive; the packaging step likely dropped the manifest.","If publishing a plugin, add the manifest at the expected location and reinstall.","Check whether the archive unpacked into an extra nested directory and flatten it."],"exampleFix":"// before (zip layout: myplugin/dist/plugin.json)\nzip -r myplugin.zip myplugin\n// after (manifest at bundle root)\nzip -r myplugin.zip myplugin  # with myplugin/plugin.json at top level","handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\nconst SUPPORTED = ['plugin.json', '.claude-plugin/plugin.json', 'kimi.plugin.json', 'plugin.toml'];\nfunction bundleHasManifest(dir) {\n  return SUPPORTED.some((p) => fs.existsSync(`${dir}/${p}`));\n}\nif (!bundleHasManifest('dist/myplugin')) throw new Error('no manifest in bundle');","typeGuard":"function isPluginBundleDir(p) {\n  return p != null && typeof p === 'string' &&\n    ['plugin.json', 'kimi.plugin.json', 'plugin.toml'].some((f) =>\n      fs.existsSync(path.join(p, f)) || fs.existsSync(path.join(p, '.claude-plugin', f)));\n}","tryCatchPattern":"try {\n  await codewhale.plugins.install(archivePath);\n} catch (e) {\n  if (e.message.includes('no supported manifest')) {\n    console.error('bundle missing plugin.json — repack with manifest at root');\n  } else throw e;\n}","preventionTips":["Keep the manifest at the bundle root, never nested under a wrapper directory.","Verify archive contents with `tar tzf`/`unzip -l` before installing.","Add a packaging test asserting the manifest ships in the artifact."],"tags":["plugins","install","manifest","rust"],"backgroundTag":"file-not-found","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}