Files
OneOS1.2/scripts/sync-project-metadata.mjs
王冕 af29b26fe8 Sync OneOS workspace with new prototypes, annotations, and Gitea remote fix.
Add vehicle-h2-fee-ledger, customer-management, lease and self-operated ledgers, annotation sources, agent skills, and vite annotation runtime support. Update vehicle management, contract templates, and lease contract flows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 15:27:23 +08:00

652 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'],
themes: ['src/themes'],
media: ['src/resources/assets'],
};
export const resourceWriteTargets = {
prototypes: { type: 'project-relative-path', path: resourceLayout.prototypes[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,
};
export const PROTOTYPE_PLACEHOLDER_GUIDE = {
kind: 'prototype-empty',
title: '这个原型还没有开始创建',
description: '告诉 AI 你想做什么:目标用户、使用场景、页面内容和参考风格。',
steps: [
'在本地 AI 软件中打开本页面',
'打开草稿创作原型',
],
tips: [
'模型不要用 auto推荐Claude Opus 4.8、Gemini 3.1 Pro、GPT-5.5、Kimi K2.7、GLM-5.2。',
'一个任务开一个新对话,避免多个需求互相干扰。',
'多用图片和语音描述,截图、草图和参考页面通常比长文字更清楚。',
'如果已有视觉规范,建议先创建设计系统。',
],
};
// 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 hasGeneratedPlaceholderSource(indexFilePath) {
if (!fs.existsSync(indexFilePath)) return false;
const source = fs.readFileSync(indexFilePath, 'utf8');
const hasGeneratedShell = source.includes('placeholder-empty-page')
&& source.includes('打开左侧默认引导页继续创建')
&& source.includes('export default function Placeholder');
return hasGeneratedShell && (
source.includes('@axhub-placeholder prototype-empty')
|| source.includes('className="placeholder-empty-page"')
);
}
function hasEmptyCanvasFile(prototypeDir) {
const canvasPath = path.join(prototypeDir, 'canvas.excalidraw');
if (!fs.existsSync(canvasPath)) return true;
try {
const canvas = JSON.parse(fs.readFileSync(canvasPath, 'utf8'));
const elements = Array.isArray(canvas?.elements) ? canvas.elements : [];
const files = canvas?.files && typeof canvas.files === 'object' && !Array.isArray(canvas.files)
? canvas.files
: {};
return elements.length === 0 && Object.keys(files).length === 0;
} catch {
return false;
}
}
function isGeneratedEmptyPrototypePlaceholder(prototypeDir, indexFilePath) {
return hasGeneratedPlaceholderSource(indexFilePath) && hasEmptyCanvasFile(prototypeDir);
}
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 placeholder = isGeneratedEmptyPrototypePlaceholder(path.join(root, entry.name), indexFile);
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 } : {}),
...(placeholder ? { placeholder: true, placeholderGuide: PROTOTYPE_PLACEHOLDER_GUIDE } : {}),
};
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 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 themes = collectThemes(projectRoot, clientOrigin);
return {
schemaVersion: 1,
project: {
id: projectIdentity.id,
name: projectIdentity.name,
},
resources: {
prototypes,
themes,
},
navigation: {
prototypes: prototypes.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}`);
}