import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import ts from 'typescript';

import { readServerInfo } from './utils/serverInfo.mjs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export const PROJECT_ID = 'make-project';
export const PROJECT_NAME = '';
export const PRODUCT_NAME = 'Axhub Make';
export const MAKE_CLIENT_MARKER_KIND = 'axhub-make-client';
export const MAKE_CLIENT_MARKER_RELATIVE_PATH = '.axhub/make/client.json';
export const DEFAULT_CLIENT_ORIGIN = 'http://localhost:51720';
export const DETERMINISTIC_UPDATED_AT = '2026-05-03T00:00:00.000Z';

export const resourceLayout = {
  prototypes: ['src/prototypes'],
  docs: ['src/resources'],
  themes: ['src/themes'],
  media: ['src/resources/assets'],
};

export const resourceWriteTargets = {
  prototypes: { type: 'project-relative-path', path: resourceLayout.prototypes[0] },
  docs: { type: 'project-relative-path', path: resourceLayout.docs[0] },
  themes: { type: 'project-relative-path', path: resourceLayout.themes[0] },
  media: { type: 'project-relative-path', path: resourceLayout.media[0] },
};

export const localExportCapabilities = {
  html: false,
  make: false,
};

// Snapshot from https://getdesign.md/api/cli/downloads?brands=...
const GETDESIGN_DOWNLOAD_SNAPSHOT_DATE = '2026-05-15';
const GETDESIGN_THEME_STATS_BY_ID = {
  'airbnb': { sourceSlug: 'airbnb', downloads: 7111 },
  'airtable': { sourceSlug: 'airtable', downloads: 3446 },
  'apple': { sourceSlug: 'apple', downloads: 13995 },
  'binance': { sourceSlug: 'binance', downloads: 2179 },
  'bmw': { sourceSlug: 'bmw', downloads: 2917 },
  'bmw-m': { sourceSlug: 'bmw-m', downloads: 555 },
  'bugatti': { sourceSlug: 'bugatti', downloads: 1473 },
  'cal-com': { sourceSlug: 'cal', downloads: 3691 },
  'claude': { sourceSlug: 'claude', downloads: 10192 },
  'clay': { sourceSlug: 'clay', downloads: 3383 },
  'clickhouse': { sourceSlug: 'clickhouse', downloads: 2495 },
  'cohere': { sourceSlug: 'cohere', downloads: 3083 },
  'coinbase': { sourceSlug: 'coinbase', downloads: 3399 },
  'composio': { sourceSlug: 'composio', downloads: 2346 },
  'cursor': { sourceSlug: 'cursor', downloads: 4650 },
  'elevenlabs': { sourceSlug: 'elevenlabs', downloads: 3438 },
  'expo': { sourceSlug: 'expo', downloads: 2457 },
  'ferrari': { sourceSlug: 'ferrari', downloads: 3158 },
  'figma': { sourceSlug: 'figma', downloads: 4351 },
  'framer': { sourceSlug: 'framer', downloads: 3781 },
  'hashicorp': { sourceSlug: 'hashicorp', downloads: 2498 },
  'ibm': { sourceSlug: 'ibm', downloads: 3093 },
  'intercom': { sourceSlug: 'intercom', downloads: 3033 },
  'june': { sourceSlug: 'june', downloads: 0 },
  'kraken': { sourceSlug: 'kraken', downloads: 2738 },
  'lamborghini': { sourceSlug: 'lamborghini', downloads: 2632 },
  'linear': { sourceSlug: 'linear.app', downloads: 12010 },
  'lovable': { sourceSlug: 'lovable', downloads: 2907 },
  'mastercard': { sourceSlug: 'mastercard', downloads: 1275 },
  'meta': { sourceSlug: 'meta', downloads: 1927 },
  'minimax': { sourceSlug: 'minimax', downloads: 2742 },
  'mintlify': { sourceSlug: 'mintlify', downloads: 3271 },
  'miro': { sourceSlug: 'miro', downloads: 2587 },
  'mistral-ai': { sourceSlug: 'mistral.ai', downloads: 2481 },
  'mongodb': { sourceSlug: 'mongodb', downloads: 2399 },
  'nike': { sourceSlug: 'nike', downloads: 2158 },
  'notion': { sourceSlug: 'notion', downloads: 11442 },
  'nvidia': { sourceSlug: 'nvidia', downloads: 2610 },
  'ollama': { sourceSlug: 'ollama', downloads: 2602 },
  'opencode': { sourceSlug: 'opencode.ai', downloads: 3033 },
  'pinterest': { sourceSlug: 'pinterest', downloads: 2837 },
  'playstation': { sourceSlug: 'playstation', downloads: 1507 },
  'posthog': { sourceSlug: 'posthog', downloads: 2949 },
  'renault': { sourceSlug: 'renault', downloads: 2186 },
  'replicate': { sourceSlug: 'replicate', downloads: 2267 },
  'revolut': { sourceSlug: 'revolut', downloads: 3289 },
  'runway': { sourceSlug: 'runwayml', downloads: 2496 },
  'sanity': { sourceSlug: 'sanity', downloads: 2322 },
  'sentry': { sourceSlug: 'sentry', downloads: 2949 },
  'shopify': { sourceSlug: 'shopify', downloads: 2063 },
  'slack': { sourceSlug: 'slack', downloads: 0 },
  'spacex': { sourceSlug: 'spacex', downloads: 3053 },
  'spotify': { sourceSlug: 'spotify', downloads: 4283 },
  'starbucks': { sourceSlug: 'starbucks', downloads: 1100 },
  'stripe': { sourceSlug: 'stripe', downloads: 9401 },
  'supabase': { sourceSlug: 'supabase', downloads: 4044 },
  'superhuman': { sourceSlug: 'superhuman', downloads: 3186 },
  'tesla': { sourceSlug: 'tesla', downloads: 3228 },
  'the-verge': { sourceSlug: 'theverge', downloads: 1508 },
  'together-ai': { sourceSlug: 'together.ai', downloads: 2352 },
  'uber': { sourceSlug: 'uber', downloads: 2933 },
  'vercel': { sourceSlug: 'vercel', downloads: 10946 },
  'vodafone': { sourceSlug: 'vodafone', downloads: 750 },
  'voltagent': { sourceSlug: 'voltagent', downloads: 2847 },
  'warp': { sourceSlug: 'warp', downloads: 2541 },
  'webflow': { sourceSlug: 'webflow', downloads: 2596 },
  'wired': { sourceSlug: 'wired', downloads: 1488 },
  'wise': { sourceSlug: 'wise', downloads: 3274 },
  'xai': { sourceSlug: 'x.ai', downloads: 2599 },
  'zapier': { sourceSlug: 'zapier', downloads: 2597 },
};

function toPosix(input) {
  return String(input || '').replace(/\\/g, '/');
}

function sortById(left, right) {
  return left.id.localeCompare(right.id);
}

function getThemeStats(themeId) {
  return GETDESIGN_THEME_STATS_BY_ID[themeId] || null;
}

function sortThemesByGetDesignDownloads(left, right) {
  const leftStats = getThemeStats(left.id);
  const rightStats = getThemeStats(right.id);
  if (leftStats && rightStats) {
    return rightStats.downloads - leftStats.downloads || sortById(left, right);
  }
  if (leftStats) return -1;
  if (rightStats) return 1;
  return sortById(left, right);
}

function readJson(filePath) {
  if (!fs.existsSync(filePath)) return null;
  try {
    return JSON.parse(fs.readFileSync(filePath, 'utf8'));
  } catch {
    return null;
  }
}

function stringValue(value) {
  return typeof value === 'string' ? value.trim() : '';
}

function isLegacyOfficialProjectName(projectId, name) {
  return projectId === PROJECT_ID && (name === 'Axhub Make' || name === 'Axhub-Make');
}

export function normalizeMakeClientProjectIdentity(project) {
  const rawProject = project && typeof project === 'object' && !Array.isArray(project)
    ? project
    : {};
  const id = stringValue(rawProject.id) || PROJECT_ID;
  const name = typeof rawProject.name === 'string' ? rawProject.name.trim() : '';
  return {
    id,
    name: isLegacyOfficialProjectName(id, name) ? '' : name,
  };
}

const PAGE_ID_RE = /^[a-z0-9-]+$/u;

function normalizePageId(value) {
  const id = stringValue(value);
  return PAGE_ID_RE.test(id) ? id : '';
}

export function readMakeClientProjectIdentity(projectRoot) {
  const marker = readJson(path.join(projectRoot, MAKE_CLIENT_MARKER_RELATIVE_PATH));
  const project = marker?.project && typeof marker.project === 'object' && !Array.isArray(marker.project)
    ? marker.project
    : {};
  const id = stringValue(project.id);
  if (marker?.schemaVersion !== 1 || marker?.kind !== MAKE_CLIENT_MARKER_KIND || !id) {
    return {
      id: PROJECT_ID,
      name: PROJECT_NAME,
    };
  }
  return normalizeMakeClientProjectIdentity(project);
}

function writeJsonAtomic(filePath, value) {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
  try {
    fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
    fs.renameSync(tempPath, filePath);
  } finally {
    if (fs.existsSync(tempPath)) {
      fs.unlinkSync(tempPath);
    }
  }
}

function listFiles(rootDir, predicate) {
  if (!fs.existsSync(rootDir)) return [];
  const result = [];
  for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
    if (entry.name.startsWith('.')) continue;
    const fullPath = path.join(rootDir, entry.name);
    if (entry.isDirectory()) {
      result.push(...listFiles(fullPath, predicate));
      continue;
    }
    if (entry.isFile() && predicate(fullPath)) {
      result.push(fullPath);
    }
  }
  return result.sort((a, b) => toPosix(a).localeCompare(toPosix(b)));
}

function isIgnoredResourceRelativePath(relativePath) {
  const normalized = toPosix(relativePath).replace(/^\/+|\/+$/g, '');
  if (!normalized) return true;
  if (normalized.toLowerCase() === 'readme.md') return true;
  return normalized.split('/').some((segment) => segment.startsWith('.'));
}

function readDisplayName(indexFilePath, fallback) {
  if (!fs.existsSync(indexFilePath)) return fallback;
  const source = fs.readFileSync(indexFilePath, 'utf8');
  const displayName = source.match(/@name\s+([^\n]+)/)?.[1]?.replace(/\*\/\s*$/u, '').trim();
  return displayName || fallback;
}

function getLiteralPropertyValue(objectLiteral, propertyName) {
  const property = objectLiteral.properties.find((candidate) => (
    ts.isPropertyAssignment(candidate)
    && (
      (ts.isIdentifier(candidate.name) && candidate.name.text === propertyName)
      || (ts.isStringLiteral(candidate.name) && candidate.name.text === propertyName)
    )
  ));
  if (!property || !ts.isPropertyAssignment(property)) {
    return null;
  }
  const initializer = property.initializer;
  return ts.isStringLiteral(initializer) || ts.isNoSubstitutionTemplateLiteral(initializer)
    ? initializer.text.trim()
    : null;
}

function extractHashRouteFromCall(callExpression) {
  const expression = callExpression.expression;
  if (!ts.isIdentifier(expression) || expression.text !== 'defineHashPageRoute') {
    return null;
  }

  const pagesArg = callExpression.arguments[0];
  if (!pagesArg || !ts.isArrayLiteralExpression(pagesArg)) {
    return null;
  }

  const pages = [];
  for (const element of pagesArg.elements) {
    if (!ts.isObjectLiteralExpression(element)) {
      continue;
    }
    const id = normalizePageId(getLiteralPropertyValue(element, 'id'));
    const title = stringValue(getLiteralPropertyValue(element, 'title'));
    if (id && title) {
      pages.push({ id, title });
    }
  }
  if (!pages.length) {
    return null;
  }

  const optionsArg = callExpression.arguments[1];
  const requestedDefaultPageId = optionsArg && ts.isObjectLiteralExpression(optionsArg)
    ? normalizePageId(getLiteralPropertyValue(optionsArg, 'defaultPageId'))
    : '';
  const defaultPageId = pages.some((page) => page.id === requestedDefaultPageId)
    ? requestedDefaultPageId
    : pages[0].id;

  return {
    pages,
    defaultPageId,
  };
}

function extractHashRouteFromFile(filePath) {
  const sourceText = fs.readFileSync(filePath, 'utf8');
  const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, filePath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
  let route = null;

  const visit = (node) => {
    if (route) {
      return;
    }
    if (ts.isCallExpression(node)) {
      route = extractHashRouteFromCall(node);
      if (route) {
        return;
      }
    }
    ts.forEachChild(node, visit);
  };

  visit(sourceFile);
  return route;
}

function extractHashRouteMetadata(prototypeDir) {
  const files = listFiles(prototypeDir, (filePath) => /\.(tsx?|mts|cts)$/iu.test(filePath));
  for (const filePath of files) {
    try {
      const route = extractHashRouteFromFile(filePath);
      if (route) {
        return route;
      }
    } catch {
      // Ignore malformed source and leave the prototype without page metadata.
    }
  }
  return null;
}

function titleFromMarkdown(markdownPath, fallback) {
  if (!fs.existsSync(markdownPath)) return fallback;
  const source = fs.readFileSync(markdownPath, 'utf8');
  const heading = source.match(/^#\s+(.+)$/mu)?.[1]?.trim();
  return heading || fallback;
}

function titleFromTokenFile(tokenPath, fallback) {
  const token = readJson(tokenPath);
  if (token && typeof token === 'object') {
    const title = typeof token.title === 'string'
      ? token.title.trim()
      : typeof token.name === 'string'
        ? token.name.trim()
        : '';
    if (title) return title;
  }
  return fallback;
}

function normalizeThemeResourceTitle(title, fallback) {
  const rawTitle = typeof title === 'string' ? title.trim() : '';
  const fallbackTitle = typeof fallback === 'string' ? fallback.trim() : '';
  if (!rawTitle) return fallbackTitle;

  const repeatedThemeTitle = rawTitle.match(/^(.+?)\s+主题\s*-\s*(.+)$/u);
  if (repeatedThemeTitle) {
    const before = repeatedThemeTitle[1]?.trim();
    const after = repeatedThemeTitle[2]?.trim();
    if (before && after && before.toLowerCase() === after.toLowerCase()) {
      return after;
    }
  }

  return rawTitle;
}

function createAxureArtifactMetadata(projectRoot, prototypeName) {
  const artifactRoot = path.join(projectRoot, '.axhub/make/artifacts/axure', prototypeName);
  const files = {
    manifestPath: '.axhub/make/artifacts/axure/{name}/manifest.json',
    indexBundlePath: '.axhub/make/artifacts/axure/{name}/index-bundle.json',
    axureJsonPath: '.axhub/make/artifacts/axure/{name}/axure-json.json',
    coverSvgPath: '.axhub/make/artifacts/axure/{name}/cover.svg',
  };
  const existing = {};
  for (const [key, template] of Object.entries(files)) {
    const relativePath = template.replace('{name}', prototypeName);
    if (fs.existsSync(path.join(projectRoot, relativePath))) {
      existing[key] = relativePath;
    }
  }
  if (!Object.keys(existing).length && !fs.existsSync(artifactRoot)) {
    return undefined;
  }
  return {
    axure: {
      caseId: prototypeName,
      ...existing,
    },
  };
}

function createFigmaArtifactMetadata(projectRoot, prototypeName) {
  const artifactRoot = path.join(projectRoot, '.axhub/make/artifacts/figma', prototypeName);
  const files = {
    canvasFigPath: '.axhub/make/artifacts/figma/{name}/canvas.fig',
    metaPath: '.axhub/make/artifacts/figma/{name}/meta.json',
    aiChatPath: '.axhub/make/artifacts/figma/{name}/ai_chat.json',
    codeManifestPath: '.axhub/make/artifacts/figma/{name}/canvas.code-manifest.json',
    manifestPath: '.axhub/make/artifacts/figma/{name}/manifest.json',
    thumbnailPath: '.axhub/make/artifacts/figma/{name}/thumbnail.png',
  };
  const existing = {};
  for (const [key, template] of Object.entries(files)) {
    const relativePath = template.replace('{name}', prototypeName);
    if (fs.existsSync(path.join(projectRoot, relativePath))) {
      existing[key] = relativePath;
    }
  }
  const imagesRelativePath = `.axhub/make/artifacts/figma/${prototypeName}/images`;
  if (fs.existsSync(path.join(projectRoot, imagesRelativePath))) {
    existing.imagesDir = imagesRelativePath;
  }
  if (!Object.keys(existing).length && !fs.existsSync(artifactRoot)) {
    return undefined;
  }
  return {
    figma: {
      resourceId: prototypeName,
      ...existing,
    },
  };
}

function readRuntimeEntryKeys(projectRoot) {
  const manifest = readJson(path.join(projectRoot, '.axhub/make/entries.json'));
  const items = manifest && typeof manifest === 'object' && manifest.items && typeof manifest.items === 'object'
    ? manifest.items
    : {};
  const legacyJs = manifest && typeof manifest === 'object' && manifest.js && typeof manifest.js === 'object'
    ? manifest.js
    : {};
  const keys = new Set([
    ...Object.keys(items),
    ...Object.keys(legacyJs),
  ]);

  return Array.from(keys)
    .map((key) => {
      const normalizedKey = toPosix(key).replace(/^\/+/, '').replace(/\/+$/, '');
      const item = items[key];
      const group = stringValue(item?.group) || normalizedKey.split('/')[0] || '';
      const name = stringValue(item?.name) || normalizedKey.split('/').slice(1).join('/') || '';
      if (!normalizedKey || group !== 'prototypes' || !name) {
        return null;
      }
      return { key: normalizedKey, name };
    })
    .filter(Boolean);
}

function createRuntimeArtifactMetadata(projectRoot, prototypeName, runtimeEntryKeys, options = {}) {
  if (options.includeRuntimeArtifacts === false) {
    return undefined;
  }
  const entry = runtimeEntryKeys.find((item) => item.name === prototypeName || item.key === `prototypes/${prototypeName}`);
  if (!entry) {
    return undefined;
  }
  const builtJsPath = `dist/${entry.key}.js`;
  if (!fs.existsSync(path.join(projectRoot, builtJsPath))) {
    return undefined;
  }
  return {
    runtime: {
      builtJsPath,
    },
  };
}

function createResourceClientUrl(clientOrigin, resourceKind, resourceName) {
  const pathname = `/${resourceKind}/${encodeURIComponent(resourceName)}`;
  const normalizedOrigin = String(clientOrigin || '').trim().replace(/\/+$/u, '');
  if (!normalizedOrigin) {
    return pathname;
  }
  return `${normalizedOrigin}${pathname}`;
}

function collectPrototypes(projectRoot, clientOrigin, options = {}) {
  const roots = resourceLayout.prototypes.map((dir) => path.join(projectRoot, dir));
  const runtimeEntryKeys = readRuntimeEntryKeys(projectRoot);
  const items = [];
  for (const root of roots) {
    if (!fs.existsSync(root)) continue;
    for (const entry of fs.readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
      if (!entry.isDirectory()) continue;
      const indexFile = path.join(root, entry.name, 'index.tsx');
      if (!fs.existsSync(indexFile)) continue;
      const filePath = toPosix(path.relative(projectRoot, indexFile));
      const route = extractHashRouteMetadata(path.join(root, entry.name));
      const item = {
        id: entry.name,
        name: entry.name,
        title: readDisplayName(indexFile, entry.name),
        clientUrl: createResourceClientUrl(clientOrigin, 'prototypes', entry.name),
        previewMode: 'clientRuntime',
        description: '',
        updatedAt: DETERMINISTIC_UPDATED_AT,
        filePath,
        ...(options.includeAbsoluteFilePaths === false ? {} : { absoluteFilePath: path.resolve(indexFile) }),
        ...(route ? { pages: route.pages, defaultPageId: route.defaultPageId } : {}),
      };
      const artifacts = {
        ...createFigmaArtifactMetadata(projectRoot, entry.name),
        ...createAxureArtifactMetadata(projectRoot, entry.name),
        ...createRuntimeArtifactMetadata(projectRoot, entry.name, runtimeEntryKeys, options),
      };
      if (Object.keys(artifacts).length > 0) {
        item.artifacts = artifacts;
      }
      items.push(item);
    }
  }
  return items.sort(sortById);
}

function collectDocs(projectRoot, options = {}) {
  const docs = [];
  for (const root of resourceLayout.docs.map((dir) => path.resolve(projectRoot, dir))) {
    for (const filePath of listFiles(root, () => true)) {
      const relativePath = toPosix(path.relative(root, filePath));
      if (isIgnoredResourceRelativePath(relativePath)) continue;
      const isMarkdown = path.extname(filePath).toLowerCase() === '.md';
      const id = isMarkdown ? relativePath.replace(/\.md$/iu, '') : relativePath;
      docs.push({
        id,
        name: id,
        title: isMarkdown
          ? titleFromMarkdown(filePath, path.basename(filePath, '.md'))
          : relativePath.replace(/\.[^.]+$/u, ''),
        path: options.includeAbsoluteFilePaths === false
          ? toPosix(path.relative(projectRoot, filePath))
          : path.resolve(filePath),
        description: '',
        updatedAt: DETERMINISTIC_UPDATED_AT,
      });
    }
  }

  return docs.sort(sortById);
}

function collectThemes(projectRoot, clientOrigin) {
  const items = [];
  for (const root of resourceLayout.themes.map((dir) => path.resolve(projectRoot, dir))) {
    if (!fs.existsSync(root)) continue;
    for (const entry of fs.readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
      if (!entry.isDirectory()) continue;
      const indexFile = path.join(root, entry.name, 'index.tsx');
      if (!fs.existsSync(indexFile)) continue;
      const tokenPath = path.join(root, entry.name, 'designToken.json');
      const getdesignStats = getThemeStats(entry.name);
      const rawTitle = readDisplayName(indexFile, titleFromTokenFile(tokenPath, entry.name));
      items.push({
        id: entry.name,
        name: entry.name,
        title: normalizeThemeResourceTitle(rawTitle, entry.name),
        clientUrl: createResourceClientUrl(clientOrigin, 'themes', entry.name),
        sourcePath: toPosix(path.relative(projectRoot, path.join(root, entry.name))),
        updatedAt: DETERMINISTIC_UPDATED_AT,
        ...(getdesignStats
          ? {
              getdesign: {
                source: 'getdesign.md',
                sourceSlug: getdesignStats.sourceSlug,
                downloads: getdesignStats.downloads,
                snapshotDate: GETDESIGN_DOWNLOAD_SNAPSHOT_DATE,
              },
            }
          : {}),
      });
    }
  }
  return items
    .sort(sortThemesByGetDesignDownloads)
    .map((item) => Object.fromEntries(Object.entries(item).filter(([, value]) => value !== undefined)));
}

export function buildMakeProjectMetadata(projectRoot, options = {}) {
  const clientOrigin = String(options.clientOrigin ?? DEFAULT_CLIENT_ORIGIN).replace(/\/+$/u, '');
  const projectIdentity = readMakeClientProjectIdentity(projectRoot);
  const prototypes = collectPrototypes(projectRoot, clientOrigin, options);
  const docs = collectDocs(projectRoot, options);
  const themes = collectThemes(projectRoot, clientOrigin);

  return {
    schemaVersion: 1,
    project: {
      id: projectIdentity.id,
      name: projectIdentity.name,
    },
    resources: {
      prototypes,
      docs,
      themes,
    },
    navigation: {
      prototypes: prototypes.map((item) => item.id),
      docs: docs.map((item) => item.id),
    },
    orders: {
      themes: themes.map((item) => item.id),
    },
    capabilities: {
      quickEdit: true,
      quickEditMode: 'clientRuntime',
      figmaExport: true,
      axureExport: true,
      localExports: localExportCapabilities,
    },
    resourceWriteTargets,
  };
}

export function resolveClientOrigin(projectRoot, fallbackOrigin = DEFAULT_CLIENT_ORIGIN) {
  const runtime = readServerInfo(projectRoot, 'runtime');
  return runtime?.origin || fallbackOrigin;
}

export function syncMakeProjectMetadata(projectRoot, options = {}) {
  const metadata = buildMakeProjectMetadata(projectRoot, {
    clientOrigin: options.includeRuntimeUrls === true
      ? options.clientOrigin ?? resolveClientOrigin(projectRoot)
      : '',
    includeAbsoluteFilePaths: options.includeAbsoluteFilePaths === true,
    includeRuntimeArtifacts: options.includeRuntimeArtifacts === true,
  });
  const metadataPath = path.join(projectRoot, '.axhub/make/project.json');
  writeJsonAtomic(metadataPath, metadata);
  return { metadata, metadataPath };
}

if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
  const appRoot = path.resolve(__dirname, '..');
  const { metadata, metadataPath } = syncMakeProjectMetadata(appRoot);
  console.log(`Synced ${metadata.project.name || 'unnamed project'} metadata: ${metadataPath}`);
}
