@codecademy/gamut

68.2.268.2.3-alpha.b8b211.0
bin/lib/claude.mjs
+bin/lib/claude.mjsNew file
+52
Index: package/bin/lib/claude.mjs
===================================================================
--- package/bin/lib/claude.mjs
+++ package/bin/lib/claude.mjs
@@ -0,0 +1,52 @@
+import { readFile } from 'node:fs/promises';
+import { join } from 'node:path';
+
+/**
+ * Reads .claude-plugin/marketplace.json and returns a "name@marketplace" plugin spec.
+ *
+ * @param {string} sourceRoot
+ * @returns {Promise<string>}
+ */
+export async function claudePluginSpec(sourceRoot) {
+  const mp = join(sourceRoot, '.claude-plugin', 'marketplace.json');
+  let text;
+  try {
+    text = await readFile(mp, 'utf8');
+  } catch {
+    throw new Error(
+      `Missing ${mp}.\n` +
+        `A .claude-plugin/marketplace.json is required for Claude Code installation.`,
+    );
+  }
+
+  const json =
+    /** @type {{ name?: string; plugins?: Array<{ name?: string; source?: string }> }} */ (
+      JSON.parse(text)
+    );
+  const { name: marketplaceName, plugins } = json;
+
+  if (!marketplaceName || !Array.isArray(plugins) || plugins.length === 0) {
+    throw new Error(`Invalid marketplace.json — needs "name" and "plugins[]": ${mp}`);
+  }
+
+  const entry =
+    plugins.find((p) => p.source === './' || p.source === '.' || p.source == null) ?? plugins[0];
+
+  if (!entry?.name) {
+    throw new Error(`No plugin name found in marketplace.json plugins[]: ${mp}`);
+  }
+
+  return `${entry.name}@${marketplaceName}`;
+}
+
+/**
+ * Returns just the marketplace name portion of a plugin spec ("name@marketplace").
+ *
+ * @param {string} spec
+ * @returns {string}
+ */
+export function marketplaceName(spec) {
+  const name = spec.split('@')[1];
+  if (!name) throw new Error(`Could not parse marketplace name from plugin spec: ${spec}`);
+  return name;
+}