/**
 * Studio-api.js — Studio filesystem integration
 *
 * REST endpoints under /__studio/* that provide server-backed file operations so the studio can
 * work universally (not just Chrome with File System Access API).
 *
 * All paths are relative to the project root. Directory traversal above root is rejected.
 */

import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
import { errorMessage } from "@jxsuite/schema/parse";
import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
import { existsSync, readFileSync, statSync } from "node:fs";
import { buildProjectFormatRegistry } from "@jxsuite/compiler/format-host";
import { applyRename } from "./refactor/apply.ts";
import {
  bunExecutable,
  dependenciesNeedInstall,
  installDependencies,
  outdatedPackages,
  setPackageVersions,
} from "./packages.ts";
import type { FormatRegistry } from "@jxsuite/schema/format-registry";
import type { ClassJsonDef } from "./types.ts";
import type { ProjectConfig } from "@jxsuite/schema/types";

/** Normalise a path to forward slashes (Windows `path` module returns backslashes). */
const fwd = (p: string) => p.replaceAll("\\", "/");

/**
 * Expand a leading `~` to the user's home directory using path.join so separators are normalised (a
 * plain string replace left the input's forward slashes intact, yielding mixed separators on
 * Windows) and falling back to USERPROFILE where HOME is unset.
 */
const expandTilde = (p: string) =>
  p.startsWith("~") ? join(process.env.HOME || process.env.USERPROFILE || "", p.slice(1)) : p;

interface PackageJson {
  name?: string;
  workspaces?: unknown;
  dependencies?: Record<string, string>;
  devDependencies?: Record<string, string>;
  customElements?: string;
  [key: string]: unknown;
}

/** A custom-element-manifest declaration (subset consumed here). */
interface CemDeclaration {
  customElement?: boolean;
  tagName?: string;
  description?: string;
  cssProperties?: unknown[];
  events?: unknown[];
  slots?: unknown[];
  members?: { kind?: string; privacy?: string; [key: string]: unknown }[];
  attributes?: {
    name?: string;
    default?: unknown;
    description?: string;
    type?: { text?: string };
  }[];
  [key: string]: unknown;
}

interface CemModule {
  path?: string;
  declarations?: CemDeclaration[];
}

interface Cem {
  modules?: CemModule[];
}

/** A parsed Jx component document (subset consumed by component discovery). */
interface ComponentDoc {
  $id?: string;
  tagName?: string;
  $elements?: unknown[];
  state?: Record<string, unknown>;
  [key: string]: unknown;
}

interface SlotDef {
  name: string;
  fallback?: unknown[];
}

/**
 * Collect slot definitions (name + fallback children) from a parsed component tree. Whitespace-only
 * names count as unnamed (""). Only static children arrays are walked.
 */
function collectSlotDefs(node: unknown, out: SlotDef[] = []): SlotDef[] {
  if (!node || typeof node !== "object" || Array.isArray(node)) {
    return out;
  }
  const el = node as Record<string, unknown>;
  if (el.tagName === "slot") {
    const attrs = el.attributes as Record<string, unknown> | undefined;
    const rawName = attrs?.name;
    const name = typeof rawName === "string" ? rawName.trim() : "";
    const { children } = el;
    out.push({
      name,
      ...(Array.isArray(children) && children.length > 0 ? { fallback: children } : {}),
    });
  }
  if (Array.isArray(el.children)) {
    for (const c of el.children) {
      collectSlotDefs(c, out);
    }
  }
  return out;
}

// ─── Format registry (per project root, invalidated on project.json change) ──

const formatRegistryCache = new Map<string, { mtime: number; registry: FormatRegistry }>();

/**
 * Build (or reuse) the format registry for a project root from its project.json imports map. An
 * empty registry is returned when there is no project.json — only .json files are handled then.
 *
 * @param {string} projectRoot
 * @returns {Promise<FormatRegistry>}
 */
async function getFormatRegistry(projectRoot: string): Promise<FormatRegistry> {
  const projectJsonPath = resolve(projectRoot, "project.json");
  let mtime = 0;
  let projectConfig: ProjectConfig | undefined;
  try {
    mtime = statSync(projectJsonPath).mtimeMs;
    projectConfig = JSON.parse(readFileSync(projectJsonPath, "utf8")) as ProjectConfig;
  } catch {
    projectConfig = undefined;
  }
  const cached = formatRegistryCache.get(projectRoot);
  if (cached && cached.mtime === mtime) {
    return cached.registry;
  }
  const registry = await buildProjectFormatRegistry(projectRoot, projectConfig);
  formatRegistryCache.set(projectRoot, { mtime, registry });
  return registry;
}

/**
 * Check that a path is under either the server root OR the active project root. This allows file
 * operations on external projects that have been explicitly activated via /__studio/activate.
 *
 * @param {string} filePath
 * @param {string} root
 * @param {string | null} activeProjectRoot
 */
function assertAccessible(filePath: string, root: string, activeProjectRoot: string | null) {
  const rel = relative(root, filePath);
  if (!rel.startsWith("..") && !rel.startsWith("/")) {
    return;
  }
  if (activeProjectRoot) {
    const relActive = relative(activeProjectRoot, filePath);
    if (!relActive.startsWith("..") && !relActive.startsWith("/")) {
      return;
    }
  }
  throw new Error("Path outside project root");
}

const statusMap: Record<string, string> = {
  A: "A",
  C: "C",
  D: "D",
  M: "M",
  R: "R",
  T: "T",
  U: "U",
};

/**
 * Parse raw `git status --porcelain=v2 --branch` output into structured data.
 *
 * @param {string} out
 * @returns {{
 *   branch: string;
 *   ahead: number;
 *   behind: number;
 *   files: { path: string; status: string; staged: boolean }[];
 *   isRepo?: boolean;
 *   remotes?: string[];
 * }}
 */
export function parseGitStatus(out: string) {
  let branch = "";
  let ahead = 0;
  let behind = 0;
  /** @type {{ path: string; status: string; staged: boolean }[]} */
  const files = [];

  for (const line of out.split("\n")) {
    if (!line) {
      continue;
    }

    if (line.startsWith("# branch.head ")) {
      branch = line.slice("# branch.head ".length);
    } else if (line.startsWith("# branch.ab ")) {
      const m = line.match(/\+(\d+) -(\d+)/);
      if (m) {
        ahead = Math.trunc(Number(m[1]!));
        behind = Math.trunc(Number(m[2]!));
      }
    } else if (line.startsWith("1 ") || line.startsWith("2 ")) {
      const parts = line.split(" ");
      const xy = parts[1]!;
      const stagedCode = xy[0]!;
      const unstagedCode = xy[1]!;
      let filePath;
      if (line.startsWith("2 ")) {
        const tabIdx = line.indexOf("\t");
        const pathPart = line.slice(tabIdx + 1);
        filePath = pathPart.split("\t").pop() || "";
      } else {
        filePath = parts.slice(8).join(" ");
      }
      if (stagedCode !== ".") {
        files.push({
          path: filePath,
          staged: true,
          status: statusMap[stagedCode] || stagedCode,
        });
      }
      if (unstagedCode !== ".") {
        files.push({
          path: filePath,
          staged: false,
          status: statusMap[unstagedCode] || unstagedCode,
        });
      }
    } else if (line.startsWith("? ")) {
      files.push({ path: line.slice(2), staged: false, status: "U" });
    }
  }

  return { ahead, behind, branch, files };
}

/**
 * Handle /__studio/* requests.
 *
 * @param {Request} req
 * @param {URL} url
 * @param {string} root
 * @param {string | null} [activeProjectRoot]
 * @returns {Promise<Response | null>}
 */
export async function handleStudioApi(
  req: Request,
  url: URL,
  root: string,
  activeProjectRoot: string | null = null,
) {
  const path = url.pathname;

  // Project metadata
  if (path === "/__studio/project" && req.method === "GET") {
    try {
      const pkg = JSON.parse(await readFile(resolve(root, "package.json"), "utf8")) as PackageJson;
      return Response.json({
        name: pkg.name ?? basename(root),
        root,
        workspaces: pkg.workspaces ?? [],
      });
    } catch {
      return Response.json({ name: basename(root), root, workspaces: [] });
    }
  }

  // Project info — probe a directory for site-project characteristics
  if (path === "/__studio/project-info" && req.method === "GET") {
    const dir = url.searchParams.get("dir") || activeProjectRoot || root;
    const absDir = isAbsolute(dir) ? dir : resolve(root, dir);
    try {
      assertAccessible(absDir, root, activeProjectRoot);
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 400 });
    }
    try {
      const projectRoot = fwd(absDir);
      const conventionalDirs = [
        "pages",
        "layouts",
        "components",
        "content",
        "data",
        "public",
        "styles",
      ];
      const directories = [];
      for (const d of conventionalDirs) {
        try {
          const s = await stat(resolve(absDir, d));
          if (s.isDirectory()) {
            directories.push(d);
          }
        } catch {}
      }

      let isSiteProject = false;
      let projectConfig: ProjectConfig | null = null;
      try {
        const raw = JSON.parse(await readFile(resolve(absDir, "project.json"), "utf8")) as unknown;
        if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
          isSiteProject = true;
          projectConfig = raw as ProjectConfig;
        }
      } catch {}

      return Response.json({
        directories,
        isSiteProject,
        projectConfig,
        projectRoot,
      });
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Resolve nearest project.json ancestor for a given file path
  if (path === "/__studio/resolve-site" && req.method === "GET") {
    const filePath = url.searchParams.get("path");
    if (!filePath) {
      return Response.json({ error: "Missing path param" }, { status: 400 });
    }
    try {
      // Walk up from file's directory looking for project.json
      const absFile = expandTilde(filePath);
      let dir = dirname(absFile);
      while (dir) {
        const candidate = resolve(dir, "project.json");
        if (existsSync(candidate)) {
          const config = JSON.parse(readFileSync(candidate, "utf8")) as ProjectConfig;
          const relPath = fwd(dir);
          const fileRelPath = fwd(relative(dir, absFile));
          return Response.json({
            fileRelPath,
            projectConfig: config,
            relPath,
            sitePath: dir,
          });
        }
        const parent = dirname(dir);
        if (parent === dir) {
          break;
        }
        dir = parent;
      }
      return Response.json({ sitePath: null });
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Find a project directory by name — searches $HOME for the first matching directory with a
  // Project.json. Dev-mode workaround for when showDirectoryPicker() can't provide absolute paths.
  if (path === "/__studio/find-project" && req.method === "GET") {
    const name = url.searchParams.get("name");
    if (!name) {
      return Response.json({ error: "Missing name" }, { status: 400 });
    }
    try {
      const home = process.env.HOME || process.env.USERPROFILE || "";
      if (!home) {
        return Response.json({ path: null });
      }
      const glob = new Bun.Glob(`**/${name}/project.json`);
      try {
        for await (const match of glob.scan({ cwd: home, dot: false })) {
          if (match.includes("node_modules") || match.includes(".Trash")) {
            continue;
          }
          const abs = resolve(home, dirname(match));
          return Response.json({ path: abs });
        }
      } catch {}
      return Response.json({ path: null });
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Discover site projects — find all project.json files under root
  if (path === "/__studio/sites" && req.method === "GET") {
    try {
      const glob = new Bun.Glob("**/project.json");
      const sites = [];
      for await (const match of glob.scan({ cwd: root, dot: false })) {
        if (
          match.includes("node_modules") ||
          fwd(match).includes("dist/") ||
          fwd(match).includes(".claude/")
        ) {
          continue;
        }
        const fp = resolve(root, match);
        try {
          const raw = JSON.parse(await readFile(fp, "utf8")) as unknown;
          if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
            const projectDir = fwd(dirname(fp));
            sites.push({ config: raw as ProjectConfig, path: projectDir });
          }
        } catch {}
      }
      return Response.json(sites);
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Create a new project
  if (path === "/__studio/create-project" && req.method === "POST") {
    try {
      const body = (await req.json()) as {
        name?: string;
        description?: string;
        url?: string;
        adapter?: "static" | "cloudflare-pages" | "cloudflare-workers" | "node" | "bun";
        directory?: string;
      };
      const { name, description, url: siteUrl, adapter, directory } = body;
      if (!name || !directory) {
        return Response.json({ error: "name and directory are required" }, { status: 400 });
      }
      const destPath = resolve(root, directory);
      assertAccessible(destPath, root, activeProjectRoot);

      const { generateProject } = await import("@jxsuite/create/generate");
      await generateProject(destPath, {
        name,
        ...(adapter !== undefined ? { adapter } : {}),
        ...(description !== undefined ? { description } : {}),
        ...(siteUrl !== undefined ? { url: siteUrl } : {}),
      });

      const config = JSON.parse(
        await readFile(resolve(destPath, "project.json"), "utf8"),
      ) as ProjectConfig;
      const projectRoot = fwd(relative(root, destPath));
      return Response.json({ config, root: projectRoot });
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // List files
  if (path === "/__studio/files" && req.method === "GET") {
    const dir = url.searchParams.get("dir") || activeProjectRoot || root;
    const pattern = url.searchParams.get("glob");
    const absDir = isAbsolute(dir) ? dir : resolve(root, dir);
    try {
      assertAccessible(absDir, root, activeProjectRoot);
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 400 });
    }

    /** Report a path relative to the active project root (or server root as fallback). */
    const reportRelative = (fp: string) => {
      if (activeProjectRoot) {
        const rel = relative(activeProjectRoot, fp);
        if (!rel.startsWith("..")) {
          return fwd(rel);
        }
      }
      return fwd(relative(root, fp));
    };

    try {
      if (pattern) {
        const glob = new Bun.Glob(pattern);
        const files = [];
        for await (const match of glob.scan({ cwd: absDir, dot: false })) {
          const fp = resolve(absDir, match);
          try {
            const s = await stat(fp);
            if (!s.isDirectory()) {
              files.push({
                modified: s.mtime.toISOString(),
                name: basename(match),
                path: reportRelative(fp),
                size: s.size,
              });
            }
          } catch {}
        }
        return Response.json(files);
      }

      const entries = await readdir(absDir, { withFileTypes: true });
      const files = [];
      for (const entry of entries) {
        if (entry.name.startsWith(".")) {
          continue;
        }
        const fp = resolve(absDir, entry.name);
        const s = await stat(fp);
        files.push({
          modified: s.mtime.toISOString(),
          name: entry.name,
          path: reportRelative(fp),
          size: s.size,
          type: entry.isDirectory() ? "directory" : "file",
        });
      }
      return Response.json(files);
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Component discovery — scan project for custom element definitions
  if (path === "/__studio/components" && req.method === "GET") {
    const dir = url.searchParams.get("dir") || activeProjectRoot || root;
    const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
    try {
      assertAccessible(scanRoot, root, activeProjectRoot);
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 400 });
    }
    try {
      const registry = await getFormatRegistry(scanRoot);
      const componentExts = registry.documentExtensions("component").map((e: string) => e.slice(1));
      const glob = new Bun.Glob(`**/*.{${["json", ...componentExts].join(",")}}`);
      const components = [];
      for await (const match of glob.scan({ cwd: scanRoot, dot: false })) {
        if (
          match.includes("node_modules") ||
          fwd(match).includes("dist/") ||
          fwd(match).includes(".claude/")
        ) {
          continue;
        }
        const fp = resolve(scanRoot, match);
        try {
          let content: ComponentDoc;
          if (match.endsWith(".json")) {
            content = JSON.parse(await readFile(fp, "utf8")) as ComponentDoc;
          } else {
            const entry = registry.byExtension(extname(match), "parse");
            if (!entry) {
              continue;
            }
            const source = await readFile(fp, "utf8");
            content = (await entry.call("parse", source)) as ComponentDoc;
          }
          if (content.tagName && content.tagName.includes("-")) {
            const slotDefs = collectSlotDefs(content);
            components.push({
              $id: content.$id || null,
              hasElements: Array.isArray(content.$elements) && content.$elements.length > 0,
              path: fwd(match),
              props: Object.entries(content.state || {})
                .filter(([, d]) => {
                  if (d == null) {
                    return false;
                  }
                  // Shorthand: "key": "value" or "key": 0 etc.
                  if (typeof d !== "object") {
                    return true;
                  }
                  // Full form: skip computed/handler/prototype entries
                  const obj = d as Record<string, unknown>;
                  return !obj.$prototype && !obj.$handler && !obj.$compute;
                })
                .map(([name, d]) => {
                  if (typeof d !== "object" || d === null) {
                    // Shorthand: infer type from value
                    return { default: d, name, type: typeof d };
                  }
                  const obj = d as Record<string, unknown>;
                  return {
                    default: obj.default,
                    format: obj.format,
                    name,
                    type: obj.type,
                  };
                }),
              ...(slotDefs.length > 0 ? { slots: slotDefs } : {}),
              source: "jx",
              tagName: content.tagName,
            });
          }
        } catch {} // Skip non-JSON or parse errors
      }

      // Discover CEM-bearing npm packages
      try {
        const projectPkgPath = resolve(scanRoot, "package.json");
        if (existsSync(projectPkgPath)) {
          const pkg = JSON.parse(await readFile(projectPkgPath, "utf8")) as PackageJson;
          const deps = { ...pkg.dependencies, ...pkg.devDependencies };
          for (const name of Object.keys(deps)) {
            try {
              const depPkgPath = resolve(
                scanRoot,
                "node_modules",
                ...name.split("/"),
                "package.json",
              );
              // Fall back to root node_modules for hoisted packages
              const fallbackPath = resolve(
                root,
                "node_modules",
                ...name.split("/"),
                "package.json",
              );
              const actualPath = existsSync(depPkgPath)
                ? depPkgPath
                : existsSync(fallbackPath)
                  ? fallbackPath
                  : null;
              if (!actualPath) {
                continue;
              }
              const depPkg = JSON.parse(await readFile(actualPath, "utf8")) as PackageJson;
              if (!depPkg.customElements) {
                continue;
              }
              const cemPath = resolve(dirname(actualPath), depPkg.customElements);
              if (!existsSync(cemPath)) {
                continue;
              }
              const cem = JSON.parse(await readFile(cemPath, "utf8")) as Cem;
              for (const mod of cem.modules || []) {
                for (const decl of mod.declarations || []) {
                  if (decl.customElement && decl.tagName) {
                    components.push({
                      $id: null,
                      cssProperties: decl.cssProperties || [],
                      description: decl.description || null,
                      events: decl.events || [],
                      hasElements: false,
                      members: (decl.members || []).filter(
                        (m) => m.kind === "field" && m.privacy !== "private",
                      ),
                      modulePath: mod.path,
                      package: name,
                      path: null,
                      props: (decl.attributes || []).map((a) => ({
                        default: a.default,
                        description: a.description || null,
                        name: a.name,
                        type: a.type?.text,
                      })),
                      slots: decl.slots || [],
                      source: "npm",
                      tagName: decl.tagName,
                    });
                  }
                }
              }
            } catch {} // Skip packages without valid CEM
          }
        }
      } catch {} // Skip if no project package.json

      return Response.json(components);
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // ─── Package management ──────────────────────────────────────────────────────

  // List CEM-bearing npm packages
  if (path === "/__studio/packages" && req.method === "GET") {
    const dir = url.searchParams.get("dir") || activeProjectRoot || root;
    const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
    try {
      const pkgPath = resolve(scanRoot, "package.json");
      if (!existsSync(pkgPath)) {
        return Response.json([]);
      }
      const pkg = JSON.parse(await readFile(pkgPath, "utf8")) as PackageJson;
      const deps = { ...pkg.dependencies, ...pkg.devDependencies };
      const packages: {
        name: string;
        version: string;
        dev: boolean;
        hasCem: boolean;
        customElementsPath: string | null;
      }[] = [];
      for (const [name, version] of Object.entries(deps)) {
        const depPkgPath = resolve(scanRoot, "node_modules", ...name.split("/"), "package.json");
        const fallbackPath = resolve(root, "node_modules", ...name.split("/"), "package.json");
        const actualPath = existsSync(depPkgPath)
          ? depPkgPath
          : existsSync(fallbackPath)
            ? fallbackPath
            : null;
        if (!actualPath) {
          continue;
        }
        try {
          const depPkg = JSON.parse(await readFile(actualPath, "utf8")) as PackageJson;
          packages.push({
            customElementsPath: depPkg.customElements || null,
            dev: pkg.dependencies?.[name] === undefined,
            hasCem: Boolean(depPkg.customElements),
            name,
            version,
          });
        } catch {}
      }
      return Response.json(packages);
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Read CEM from a specific package
  if (path === "/__studio/cem" && req.method === "GET") {
    const pkg = url.searchParams.get("pkg");
    if (!pkg) {
      return new Response("Missing pkg", { status: 400 });
    }
    const dir = url.searchParams.get("dir") || activeProjectRoot || root;
    const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
    try {
      const depPkgPath = resolve(scanRoot, "node_modules", ...pkg.split("/"), "package.json");
      const fallbackPath = resolve(root, "node_modules", ...pkg.split("/"), "package.json");
      const actualPath = existsSync(depPkgPath)
        ? depPkgPath
        : existsSync(fallbackPath)
          ? fallbackPath
          : null;
      if (!actualPath) {
        return Response.json({ cem: null });
      }
      const depPkg = JSON.parse(await readFile(actualPath, "utf8")) as PackageJson;
      if (!depPkg.customElements) {
        return Response.json({ cem: null });
      }
      const cemPath = resolve(dirname(actualPath), depPkg.customElements);
      if (!existsSync(cemPath)) {
        return Response.json({ cem: null });
      }
      const cem = JSON.parse(await readFile(cemPath, "utf8")) as Cem;
      return Response.json({ cem });
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Add an npm package
  if (path === "/__studio/packages/add" && req.method === "POST") {
    try {
      const body = (await req.json()) as { name?: string; dir?: string; dev?: boolean };
      const { name } = body;
      if (!name || typeof name !== "string") {
        return Response.json({ error: "Missing name" }, { status: 400 });
      }
      const dir = body.dir || activeProjectRoot;
      const cwd = dir ? (isAbsolute(dir) ? dir : resolve(root, dir)) : root;
      const args = ["add", name];
      if (body.dev) {
        args.splice(1, 0, "-d");
      }
      const proc = Bun.spawn([bunExecutable(), ...args], {
        cwd,
        stderr: "pipe",
        stdout: "pipe",
      });
      const exitCode = await proc.exited;
      if (exitCode !== 0) {
        const stderr = await new Response(proc.stderr).text();
        return Response.json(
          { error: stderr || `bun add exited with ${exitCode}` },
          { status: 500 },
        );
      }
      return Response.json({ ok: true });
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Remove an npm package
  if (path === "/__studio/packages/remove" && req.method === "POST") {
    try {
      const body = (await req.json()) as { name?: string; dir?: string };
      const { name } = body;
      if (!name || typeof name !== "string") {
        return Response.json({ error: "Missing name" }, { status: 400 });
      }
      const dir = body.dir || activeProjectRoot;
      const cwd = dir ? (isAbsolute(dir) ? dir : resolve(root, dir)) : root;
      const proc = Bun.spawn([bunExecutable(), "remove", name], {
        cwd,
        stderr: "pipe",
        stdout: "pipe",
      });
      const exitCode = await proc.exited;
      if (exitCode !== 0) {
        const stderr = await new Response(proc.stderr).text();
        return Response.json(
          { error: stderr || `bun remove exited with ${exitCode}` },
          { status: 500 },
        );
      }
      return Response.json({ ok: true });
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Install all dependencies (bun install)
  if (path === "/__studio/packages/install" && req.method === "POST") {
    try {
      const body = (await req.json().catch(() => ({}))) as { dir?: string };
      const dir = body.dir || activeProjectRoot || root;
      const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
      return Response.json(await installDependencies(scanRoot));
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Whether dependencies need installing (node_modules missing)
  if (path === "/__studio/packages/needs-install" && req.method === "GET") {
    const dir = url.searchParams.get("dir") || activeProjectRoot || root;
    const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
    return Response.json({ needsInstall: dependenciesNeedInstall(scanRoot) });
  }

  // Dependencies with a newer version available
  if (path === "/__studio/packages/outdated" && req.method === "GET") {
    try {
      const dir = url.searchParams.get("dir") || activeProjectRoot || root;
      const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
      return Response.json(await outdatedPackages(scanRoot));
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Set version ranges for one or more packages, then reinstall
  if (path === "/__studio/packages/set-versions" && req.method === "POST") {
    try {
      const body = (await req.json()) as {
        dir?: string;
        updates?: { name: string; version: string; dev?: boolean }[];
      };
      if (!Array.isArray(body.updates)) {
        return Response.json({ error: "Missing updates" }, { status: 400 });
      }
      const dir = body.dir || activeProjectRoot || root;
      const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
      return Response.json(await setPackageVersions(scanRoot, body.updates));
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Read file
  if (path === "/__studio/file" && req.method === "GET") {
    const fp = url.searchParams.get("path");
    if (!fp) {
      return new Response("Missing path", { status: 400 });
    }
    const abs = expandTilde(fp);
    try {
      assertAccessible(abs, root, activeProjectRoot);
    } catch (error) {
      return new Response(errorMessage(error), { status: 400 });
    }
    try {
      return Response.json(
        {
          content: await readFile(abs, "utf8"),
          path: fp,
        },
        { headers: { "Cache-Control": "public, max-age=5" } },
      );
    } catch (error) {
      return (error as NodeJS.ErrnoException).code === "ENOENT"
        ? new Response("Not found", { status: 404 })
        : Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Write file
  if (path === "/__studio/file" && req.method === "PUT") {
    const fp = url.searchParams.get("path");
    if (!fp) {
      return new Response("Missing path", { status: 400 });
    }
    const abs = resolve(root, fp);
    try {
      assertAccessible(abs, root, activeProjectRoot);
    } catch (error) {
      return new Response(errorMessage(error), { status: 400 });
    }
    try {
      await mkdir(dirname(abs), { recursive: true });
      await writeFile(abs, await req.text(), "utf8");
      return Response.json({ ok: true, path: fwd(relative(root, abs)) });
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Upload binary file
  if (path === "/__studio/file/upload" && req.method === "POST") {
    const fp = url.searchParams.get("path");
    if (!fp) {
      return new Response("Missing path", { status: 400 });
    }
    const abs = resolve(root, fp);
    try {
      assertAccessible(abs, root, activeProjectRoot);
    } catch (error) {
      return new Response(errorMessage(error), { status: 400 });
    }
    try {
      await mkdir(dirname(abs), { recursive: true });
      const buffer = await req.arrayBuffer();
      await Bun.write(abs, new Uint8Array(buffer));
      return Response.json({ ok: true, path: fwd(relative(root, abs)) });
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Delete file
  if (path === "/__studio/file" && req.method === "DELETE") {
    const fp = url.searchParams.get("path");
    if (!fp) {
      return new Response("Missing path", { status: 400 });
    }
    const abs = resolve(root, fp);
    try {
      assertAccessible(abs, root, activeProjectRoot);
    } catch (error) {
      return new Response(errorMessage(error), { status: 400 });
    }
    try {
      await unlink(abs);
      return Response.json({ ok: true, path: fwd(relative(root, abs)) });
    } catch (error) {
      return (error as NodeJS.ErrnoException).code === "ENOENT"
        ? new Response("Not found", { status: 404 })
        : Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Rename file
  if (path === "/__studio/file/rename" && req.method === "POST") {
    let body: { from?: string; to?: string };
    try {
      body = (await req.json()) as { from?: string; to?: string };
    } catch {
      return new Response("Invalid JSON", { status: 400 });
    }
    const { from, to } = body;
    if (!from || !to) {
      return new Response("Missing from or to", { status: 400 });
    }
    const absFrom = resolve(root, from);
    const absTo = resolve(root, to);
    try {
      assertAccessible(absFrom, root, activeProjectRoot);
      assertAccessible(absTo, root, activeProjectRoot);
    } catch (error) {
      return new Response(errorMessage(error), { status: 400 });
    }
    try {
      await mkdir(dirname(absTo), { recursive: true });
      await rename(absFrom, absTo);
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
    // Refactor pass: rewrite path references project-wide and, for a component, auto-rename its tag.
    // The move already succeeded, so a refactor failure is reported but never fails the rename.
    const scanRoot = activeProjectRoot ?? root;
    try {
      const registry = await getFormatRegistry(scanRoot);
      const report = await applyRename({ absFrom, absTo, registry, root: scanRoot });
      return Response.json(report);
    } catch (error) {
      return Response.json({
        error: errorMessage(error),
        from: fwd(relative(root, absFrom)),
        ok: true,
        to: fwd(relative(root, absTo)),
      });
    }
  }

  // Locate a file by name within the project root
  if (path === "/__studio/locate" && req.method === "POST") {
    let body: { name?: string };
    try {
      body = (await req.json()) as { name?: string };
    } catch {
      return new Response("Invalid JSON", { status: 400 });
    }
    const { name } = body;
    if (!name) {
      return new Response("Missing name", { status: 400 });
    }

    try {
      const glob = new Bun.Glob(`**/${name}`);
      const matches = [];
      for await (const match of glob.scan({ cwd: root, dot: false })) {
        // Skip node_modules / dist / hidden dirs
        if (match.includes("node_modules") || fwd(match).includes("dist/")) {
          continue;
        }
        matches.push(fwd(match));
      }
      if (matches.length === 0) {
        return Response.json({ path: null });
      }
      return Response.json({
        path: matches[0],
        ...(matches.length > 1 ? { alternatives: matches } : {}),
      });
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Discover a plugin module's schema for studio form rendering
  // Format capability proxy — studio fallback for capabilities whose timing excludes "client".
  // Dispatches parse/serialize through the project's format registry by import name.
  if (path === "/__studio/format" && req.method === "POST") {
    try {
      const body = (await req.json()) as {
        format?: string;
        action?: string;
        source?: string;
        doc?: Record<string, unknown>;
        options?: Record<string, unknown>;
        dir?: string;
      };
      const { format, action, source, doc, options } = body;
      const dir = body.dir || activeProjectRoot || root;
      const projectRoot = isAbsolute(dir) ? dir : resolve(root, dir);
      assertAccessible(projectRoot, root, activeProjectRoot);

      if (!format || !action) {
        return Response.json({ error: "Missing format or action" }, { status: 400 });
      }
      if (action !== "parse" && action !== "serialize") {
        return Response.json({ error: `Unsupported action "${action}"` }, { status: 400 });
      }
      const registry = await getFormatRegistry(projectRoot);
      const entry = registry.byName(format);
      if (!entry) {
        return Response.json(
          { error: `Format "${format}" is not an imported format class` },
          { status: 404 },
        );
      }
      const result =
        action === "parse"
          ? await entry.call("parse", source ?? "", options)
          : await entry.call("serialize", doc ?? {}, options);
      return Response.json({ result });
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  // Format registry listing — lets the studio introspect available formats without
  // Fetching each .class.json itself.
  if (path === "/__studio/formats" && req.method === "GET") {
    const dir = url.searchParams.get("dir") || activeProjectRoot || root;
    const projectRoot = isAbsolute(dir) ? dir : resolve(root, dir);
    try {
      assertAccessible(projectRoot, root, activeProjectRoot);
      const registry = await getFormatRegistry(projectRoot);
      return Response.json({
        formats: registry.entries.map((e) => ({
          capabilities: e.capabilities,
          documentKinds: e.documentKinds,
          exportTarget: e.exportTarget,
          extensions: e.extensions,
          mediaType: e.mediaType,
          name: e.name,
          remote: e.remote,
          studio: e.studio,
        })),
      });
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 400 });
    }
  }

  if (path === "/__studio/plugin-schema" && req.method === "GET") {
    const src = url.searchParams.get("src");
    const prototype = url.searchParams.get("prototype");
    const base = url.searchParams.get("base");
    if (!src) {
      return new Response("Missing src param", { status: 400 });
    }

    let moduleAbsPath;
    try {
      if (src.startsWith("./") || src.startsWith("../")) {
        // Relative path
        if (base) {
          const docUrlPath = new URL(base).pathname;
          const docDir = docUrlPath.slice(0, docUrlPath.lastIndexOf("/") + 1);
          moduleAbsPath = resolve(resolve(root, `.${docDir}`), src);
        } else {
          moduleAbsPath = resolve(activeProjectRoot || root, src);
          if (!existsSync(moduleAbsPath) && activeProjectRoot) {
            moduleAbsPath = resolve(root, src);
          }
        }
      } else {
        // Npm/bare specifier — use createRequire from project root, fall back to server package
        const projectRoot = activeProjectRoot || root;
        const { createRequire } = await import("node:module");
        const projRequire = createRequire(resolve(projectRoot, "package.json"));
        try {
          moduleAbsPath = projRequire.resolve(src);
        } catch {
          const serverRequire = createRequire(import.meta.url);
          moduleAbsPath = serverRequire.resolve(src);
        }
      }
    } catch (error) {
      return Response.json({
        error: errorMessage(error),
        schema: null,
      });
    }

    // .class.json: read and extract schema directly
    if (moduleAbsPath.endsWith(".class.json")) {
      try {
        const content = readFileSync(moduleAbsPath, "utf8");
        const classDef = JSON.parse(content) as ClassJsonDef;
        return Response.json({
          schema: extractStudioSchema(classDef, moduleAbsPath),
        });
      } catch (error) {
        return Response.json({
          error: errorMessage(error),
          schema: null,
        });
      }
    }

    // Sibling .class.json auto-discovery: check for <ClassName>.class.json next to the .js module
    const exportName = prototype || src;
    const classJsonPath = resolve(dirname(moduleAbsPath), `${exportName}.class.json`);
    if (existsSync(classJsonPath)) {
      try {
        const content = readFileSync(classJsonPath, "utf8");
        const classDef = JSON.parse(content) as ClassJsonDef;
        return Response.json({
          schema: extractStudioSchema(classDef, classJsonPath),
        });
      } catch {
        // Fall through to JS module import
      }
    }

    // Fallback: import JS module (backwards compat for classes without .class.json)
    try {
      const mod = (await import(moduleAbsPath)) as {
        default?: Record<string, unknown>;
        [key: string]: unknown;
      };
      const ExportedClass = mod[exportName] ?? mod.default?.[exportName];
      if (typeof ExportedClass !== "function") {
        return Response.json({
          error: `Export "${exportName}" not found`,
          schema: null,
        });
      }
      return Response.json({ schema: (ExportedClass as { schema?: unknown }).schema ?? null });
    } catch (error) {
      return Response.json({
        error: errorMessage(error),
        schema: null,
      });
    }
  }

  // ── Git endpoints ──────────────────────────────────────────────────────────

  if (path.startsWith("/__studio/git/")) {
    const cwd = activeProjectRoot || root;
    const gitCmd = path.slice("/__studio/git/".length);

    const runGit = async (args: string[]) => {
      const proc = Bun.spawn(["git", ...args], {
        cwd,
        stderr: "pipe",
        stdout: "pipe",
      });
      const exitCode = await proc.exited;
      const stdout = await new Response(proc.stdout).text();
      const stderr = await new Response(proc.stderr).text();
      if (exitCode !== 0) {
        throw new Error(stderr || `git exited with ${exitCode}`);
      }
      return stdout;
    };

    try {
      if (gitCmd === "status" && req.method === "GET") {
        // Check if we're in a git repo first
        const checkProc = Bun.spawn(["git", "rev-parse", "--is-inside-work-tree"], {
          cwd,
          stderr: "pipe",
          stdout: "pipe",
        });
        const checkExit = await checkProc.exited;
        if (checkExit !== 0) {
          return Response.json({
            ahead: 0,
            behind: 0,
            branch: "",
            files: [],
            isRepo: false,
            remotes: [],
          });
        }

        const [out, remotesOut] = await Promise.all([
          runGit(["status", "--porcelain=v2", "--branch"]),
          runGit(["remote"]),
        ]);
        const status = parseGitStatus(out);
        const fullStatus = {
          ...status,
          isRepo: true,
          remotes: remotesOut.trim().split("\n").filter(Boolean),
        };
        return Response.json(fullStatus);
      }

      if (gitCmd === "init" && req.method === "POST") {
        await runGit(["init"]);
        return Response.json({ ok: true });
      }

      if (gitCmd === "add-remote" && req.method === "POST") {
        const { name, url: remoteUrl } = (await req.json()) as {
          name?: string;
          url?: string;
        };
        if (!name || !remoteUrl) {
          return new Response("name and url required", { status: 400 });
        }
        await runGit(["remote", "add", name, remoteUrl]);
        return Response.json({ ok: true });
      }

      if (gitCmd === "branches" && req.method === "GET") {
        const out = await runGit(["branch", "--format=%(refname:short)\t%(HEAD)"]);
        let current = "";
        const branches = [];
        for (const line of out.trim().split("\n")) {
          if (!line) {
            continue;
          }
          const [name, head] = line.split("\t");
          branches.push(name!);
          if (head === "*") {
            current = name!;
          }
        }
        return Response.json({ branches, current });
      }

      if (gitCmd === "log" && req.method === "GET") {
        const limit = url.searchParams.get("limit") || "20";
        const out = await runGit(["log", `--max-count=${limit}`, "--format=%H\t%s\t%an\t%aI"]);
        const entries = out
          .trim()
          .split("\n")
          .filter(Boolean)
          .map((line) => {
            const [hash, message, author, date] = line.split("\t");
            return { author, date, hash, message };
          });
        return Response.json(entries);
      }

      if (gitCmd === "stage" && req.method === "POST") {
        const { files } = (await req.json()) as { files?: string[] };
        if (!Array.isArray(files) || files.length === 0) {
          return Response.json({ error: "Missing files" }, { status: 400 });
        }
        for (const f of files) {
          if (f.includes("..")) {
            return Response.json({ error: "Invalid path" }, { status: 400 });
          }
        }
        await runGit(["add", "--", ...files]);
        return Response.json({ ok: true });
      }

      if (gitCmd === "unstage" && req.method === "POST") {
        const { files } = (await req.json()) as { files?: string[] };
        if (!Array.isArray(files) || files.length === 0) {
          return Response.json({ error: "Missing files" }, { status: 400 });
        }
        await runGit(["restore", "--staged", "--", ...files]);
        return Response.json({ ok: true });
      }

      if (gitCmd === "commit" && req.method === "POST") {
        const { message } = (await req.json()) as { message?: string };
        if (!message || typeof message !== "string") {
          return Response.json({ error: "Missing message" }, { status: 400 });
        }
        const statusOut = await runGit(["status", "--porcelain"]);
        const hasStaged = statusOut
          .split("\n")
          .some((l) => l.length > 0 && l[0] !== " " && l[0] !== "?");
        const args = hasStaged ? ["commit", "-m", message] : ["commit", "-a", "-m", message];
        const out = await runGit(args);
        const hashMatch = out.match(/\[[\w/]+ ([a-f0-9]+)\]/);
        return Response.json({ hash: hashMatch?.[1] || "", ok: true });
      }

      if (gitCmd === "push" && req.method === "POST") {
        let body: { setUpstream?: boolean } = {};
        try {
          body = (await req.json()) as { setUpstream?: boolean };
        } catch {}
        const { setUpstream } = body;
        if (setUpstream) {
          const branchRaw = await runGit(["rev-parse", "--abbrev-ref", "HEAD"]);
          const branch = branchRaw.trim();
          await runGit(["push", "-u", "origin", branch]);
        } else {
          await runGit(["push"]);
        }
        return Response.json({ ok: true });
      }

      if (gitCmd === "pull" && req.method === "POST") {
        await runGit(["pull"]);
        return Response.json({ ok: true });
      }

      if (gitCmd === "fetch" && req.method === "POST") {
        await runGit(["fetch"]);
        return Response.json({ ok: true });
      }

      if (gitCmd === "checkout" && req.method === "POST") {
        const { branch } = (await req.json()) as { branch?: string };
        if (!branch || typeof branch !== "string") {
          return Response.json({ error: "Missing branch" }, { status: 400 });
        }
        await runGit(["checkout", branch]);
        return Response.json({ ok: true });
      }

      if (gitCmd === "create-branch" && req.method === "POST") {
        const { name } = (await req.json()) as { name?: string };
        if (!name || typeof name !== "string") {
          return Response.json({ error: "Missing name" }, { status: 400 });
        }
        await runGit(["checkout", "-b", name]);
        return Response.json({ ok: true });
      }

      if (gitCmd === "diff" && req.method === "GET") {
        const fp = url.searchParams.get("path");
        if (!fp) {
          return Response.json({ error: "Missing path" }, { status: 400 });
        }
        if (fp.includes("..")) {
          return Response.json({ error: "Invalid path" }, { status: 400 });
        }
        const diff = await runGit(["diff", "--", fp]);
        return Response.json({ diff });
      }

      if (gitCmd === "show" && req.method === "GET") {
        const fp = url.searchParams.get("path");
        const ref = url.searchParams.get("ref") || "HEAD";
        if (!fp) {
          return Response.json({ error: "Missing path" }, { status: 400 });
        }
        if (fp.includes("..")) {
          return Response.json({ error: "Invalid path" }, { status: 400 });
        }
        const content = await runGit(["show", `${ref}:${fp}`]);
        return Response.json({ content });
      }

      if (gitCmd === "discard" && req.method === "POST") {
        const { files } = (await req.json()) as { files?: string[] };
        if (!Array.isArray(files) || files.length === 0) {
          return Response.json({ error: "Missing files" }, { status: 400 });
        }
        for (const f of files) {
          if (f.includes("..")) {
            return Response.json({ error: "Invalid path" }, { status: 400 });
          }
        }
        await runGit(["checkout", "--", ...files]);
        return Response.json({ ok: true });
      }

      if (gitCmd === "clone" && req.method === "POST") {
        const { url: repoUrl } = (await req.json()) as { url?: string };
        if (!repoUrl || typeof repoUrl !== "string") {
          return Response.json({ error: "Missing url" }, { status: 400 });
        }
        const repoName = basename(repoUrl.replace(/\.git$/, ""));
        const dest = resolve(cwd, repoName);
        const proc = Bun.spawn(["git", "clone", repoUrl, dest], {
          cwd,
          stderr: "pipe",
          stdout: "pipe",
        });
        const exitCode = await proc.exited;
        const stderr = await new Response(proc.stderr).text();
        if (exitCode !== 0) {
          throw new Error(stderr || `git clone exited with ${exitCode}`);
        }
        return Response.json({ ok: true, root: dest });
      }
    } catch (error) {
      return Response.json({ error: errorMessage(error) }, { status: 500 });
    }
  }

  return null;
}

/**
 * Extract a studio-friendly schema from a .class.json definition. Transforms $defs.parameters and
 * $defs.fields into the flat { description, properties, required } shape that renderSchemaFields()
 * in the studio already consumes.
 *
 * @param {ClassJsonDef} classDef
 * @param {string} classJsonPath
 * @returns {{
 *   description: string | undefined;
 *   properties: Record<string, Record<string, unknown>>;
 *   required: string[];
 * }}
 */
function extractStudioSchema(classDef: ClassJsonDef, classJsonPath: string) {
  // If extends.$ref points to a parent, recursively merge
  let parentSchema = null;
  if (classDef.extends && typeof classDef.extends === "object" && classDef.extends.$ref) {
    try {
      const parentPath = resolve(dirname(classJsonPath), classDef.extends.$ref);
      const parentContent = readFileSync(parentPath, "utf8");
      const parentDef = JSON.parse(parentContent) as ClassJsonDef;
      parentSchema = extractStudioSchema(parentDef, parentPath);
    } catch {
      // Parent not found — proceed without inheritance
    }
  }

  const params = classDef.$defs?.parameters ?? {};
  const fields = classDef.$defs?.fields ?? {};
  const properties: Record<string, Record<string, unknown>> = {};
  const required: string[] = [];

  // Start with parent properties (child overrides)
  if (parentSchema?.properties) {
    Object.assign(properties, parentSchema.properties);
  }
  if (parentSchema?.required) {
    required.push(...parentSchema.required);
  }

  // Build properties from parameters (constructor config surface)
  for (const [key, param] of Object.entries(params)) {
    const id = param.identifier ?? key;
    const prop: Record<string, unknown> = {};
    if (param.type && typeof param.type === "object") {
      Object.assign(prop, param.type);
    }
    if (param.description) {
      prop.description = param.description;
    }
    if (param.examples) {
      prop.examples = param.examples;
    }
    if (param.format) {
      prop.format = param.format;
    }
    properties[id] = prop;
  }

  // Build properties from fields (config-visible ones only)
  for (const [key, field] of Object.entries(fields)) {
    if (field.role !== "field") {
      continue;
    }
    if (field.access === "private") {
      continue;
    }
    const id = field.identifier ?? key;
    const prop: Record<string, unknown> = {};
    if (field.type && typeof field.type === "object") {
      Object.assign(prop, field.type);
    }
    if (field.description) {
      prop.description = field.description;
    }
    if (field.default !== undefined) {
      prop.default = field.default;
    }
    if (field.initializer !== undefined && prop.default === undefined) {
      prop.default = field.initializer;
    }
    if (field.examples) {
      prop.examples = field.examples;
    }
    properties[id] = prop;
  }

  // Determine required from constructor parameters that have no default
  const ctorParams = classDef.$defs?.constructor?.parameters ?? [];
  const requiredSet = new Set<string>(required);
  for (const p of ctorParams) {
    const name = p.$ref ? p.$ref.split("/").pop() : (p.identifier ?? p.name);
    if (name && properties[name] && properties[name].default === undefined) {
      requiredSet.add(name);
    }
  }

  const resolveMethod = classDef.$defs?.methods?.resolve;

  // Surface format-extension metadata: the format block, studio hints, and a
  // Capability summary ({ parse: { timing }, serialize: { timing }, ... }).
  const def = classDef as Record<string, unknown>;
  const capabilityRoles = new Set(["parse", "serialize", "discover", "load"]);
  const capabilities: Record<string, { identifier: string; timing: string[] }> = {};
  const methods = (classDef.$defs?.methods ?? {}) as Record<
    string,
    { role?: string; identifier?: string; timing?: string[] }
  >;
  for (const [key, method] of Object.entries(methods)) {
    if (method.role && capabilityRoles.has(method.role)) {
      capabilities[method.role] = {
        identifier: method.identifier ?? key,
        timing: method.timing ?? ["compiler", "server"],
      };
    }
  }

  return {
    description: classDef.description ?? classDef.title,
    properties,
    required: [...requiredSet],
    ...(resolveMethod?.returns ? { returns: resolveMethod.returns } : {}),
    ...(def.format ? { format: def.format } : {}),
    ...(def.$studio ? { $studio: def.$studio } : {}),
    ...(Object.keys(capabilities).length > 0 ? { capabilities } : {}),
  };
}
