Optimized the root .gitignore to exclude virtual environments, node modules, and temp folders to ensure clean and lightweight version tracking. Co-authored-by: Cursor <cursoragent@cursor.com>
625 lines
22 KiB
JavaScript
Executable File
625 lines
22 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
|
||
/**
|
||
* AI Studio 项目预处理器(最小化处理模式)
|
||
*
|
||
* 只做 100% 有把握的操作:
|
||
* 1. 完整复制项目
|
||
* 2. 分析项目结构
|
||
* 3. 生成任务文档
|
||
*
|
||
* 不做任何代码修改,全部留给 AI 处理
|
||
*/
|
||
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
|
||
const TARGET_TYPE_TO_SRC_DIR = {
|
||
prototypes: 'src/prototypes',
|
||
components: 'src/components',
|
||
themes: 'src/themes',
|
||
};
|
||
|
||
const THEME_SPLIT_SKILL_DOCS = [
|
||
'/skills/axure-prototype-workflow/theme-generation.md',
|
||
'/skills/axure-prototype-workflow/doc-generation.md',
|
||
'/skills/axure-prototype-workflow/data-generation.md',
|
||
'/skills/web-page-workflow/theme-generation.md',
|
||
'/skills/web-page-workflow/doc-generation.md',
|
||
'/skills/web-page-workflow/data-generation.md',
|
||
];
|
||
|
||
const CONFIG = {
|
||
projectRoot: path.resolve(__dirname, '..'),
|
||
tempDir: path.resolve(__dirname, '../temp'),
|
||
};
|
||
|
||
function log(message, type = 'info') {
|
||
const prefix = { info: '✓', warn: '⚠', error: '✗', progress: '⏳' }[type] || 'ℹ';
|
||
console.log(`${prefix} ${message}`);
|
||
}
|
||
|
||
function ensureDir(dirPath) {
|
||
if (!fs.existsSync(dirPath)) {
|
||
fs.mkdirSync(dirPath, { recursive: true });
|
||
}
|
||
}
|
||
|
||
function sanitizeName(rawName) {
|
||
return String(rawName || '')
|
||
.replace(/[^a-z0-9-]/gi, '-')
|
||
.replace(/-+/g, '-')
|
||
.replace(/^-|-$/g, '')
|
||
.toLowerCase();
|
||
}
|
||
|
||
function getTargetInfo(targetType, outputName) {
|
||
const srcDir = TARGET_TYPE_TO_SRC_DIR[targetType];
|
||
const outputBaseDir = path.resolve(CONFIG.projectRoot, srcDir);
|
||
const outputDir = path.join(outputBaseDir, outputName);
|
||
const relativeOutputDir = `${srcDir}/${outputName}`;
|
||
|
||
if (targetType === 'themes') {
|
||
return {
|
||
targetType,
|
||
srcDir,
|
||
outputBaseDir,
|
||
outputDir,
|
||
relativeOutputDir,
|
||
tasksFileName: '.ai-studio-theme-tasks.md',
|
||
analysisFileName: '.ai-studio-theme-analysis.json',
|
||
checkPath: `/themes/${outputName}`,
|
||
label: '主题',
|
||
};
|
||
}
|
||
|
||
return {
|
||
targetType,
|
||
srcDir,
|
||
outputBaseDir,
|
||
outputDir,
|
||
relativeOutputDir,
|
||
tasksFileName: '.ai-studio-tasks.md',
|
||
analysisFileName: '.ai-studio-analysis.json',
|
||
checkPath: `/${targetType}/${outputName}`,
|
||
label: targetType === 'components' ? '组件' : '页面',
|
||
};
|
||
}
|
||
|
||
// 递归查找所有 .tsx/.ts 文件
|
||
function findFiles(dir, extensions = ['.tsx', '.ts']) {
|
||
const results = [];
|
||
|
||
if (!fs.existsSync(dir)) return results;
|
||
|
||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||
|
||
for (const entry of entries) {
|
||
const fullPath = path.join(dir, entry.name);
|
||
|
||
if (entry.isDirectory()) {
|
||
// 跳过 node_modules
|
||
if (entry.name === 'node_modules') continue;
|
||
results.push(...findFiles(fullPath, extensions));
|
||
} else {
|
||
const ext = path.extname(entry.name);
|
||
if (extensions.includes(ext)) {
|
||
results.push(fullPath);
|
||
}
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
function copyDirectory(src, dest) {
|
||
if (!fs.existsSync(src)) return 0;
|
||
ensureDir(dest);
|
||
const entries = fs.readdirSync(src, { withFileTypes: true });
|
||
let count = 0;
|
||
for (const entry of entries) {
|
||
const srcPath = path.join(src, entry.name);
|
||
const destPath = path.join(dest, entry.name);
|
||
if (entry.isDirectory()) {
|
||
if (entry.name === 'node_modules') continue;
|
||
count += copyDirectory(srcPath, destPath);
|
||
} else {
|
||
fs.copyFileSync(srcPath, destPath);
|
||
count++;
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
console.log('AI Studio Converter - Minimal Processing Mode\n');
|
||
|
||
function analyzeProject(targetDir) {
|
||
const analysis = {
|
||
files: [],
|
||
components: [],
|
||
dependencies: {},
|
||
structure: {},
|
||
indexHtml: null,
|
||
viteConfig: null
|
||
};
|
||
|
||
const files = findFiles(targetDir, ['.tsx', '.ts']);
|
||
|
||
files.forEach(file => {
|
||
const relativePath = path.relative(targetDir, file);
|
||
const content = fs.readFileSync(file, 'utf8');
|
||
const fileName = path.basename(file);
|
||
|
||
const fileInfo = {
|
||
path: relativePath,
|
||
isAppTsx: fileName === 'App.tsx',
|
||
isIndexTsx: fileName === 'index.tsx',
|
||
imports: []
|
||
};
|
||
|
||
const importMatches = content.matchAll(/import\s+.*from\s+['"]([^'"]+)['"]/g);
|
||
for (const match of importMatches) {
|
||
fileInfo.imports.push(match[1]);
|
||
}
|
||
|
||
analysis.files.push(fileInfo);
|
||
|
||
if (relativePath.startsWith('components/')) {
|
||
analysis.components.push(relativePath);
|
||
}
|
||
});
|
||
|
||
const indexHtmlPath = path.join(targetDir, 'index.html');
|
||
if (fs.existsSync(indexHtmlPath)) {
|
||
const htmlContent = fs.readFileSync(indexHtmlPath, 'utf8');
|
||
|
||
const importMapMatch = htmlContent.match(/<script type="importmap">([\s\S]*?)<\/script>/);
|
||
const importMap = importMapMatch ? JSON.parse(importMapMatch[1]) : null;
|
||
|
||
const styleMatches = htmlContent.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g);
|
||
const customStyles = Array.from(styleMatches).map(m => m[1].trim());
|
||
|
||
const fontMatches = htmlContent.matchAll(/<link[^>]*href=["']([^"']*fonts\.googleapis\.com[^"']*)["'][^>]*>/g);
|
||
const fonts = Array.from(fontMatches).map(m => m[1]);
|
||
|
||
analysis.indexHtml = {
|
||
importMap,
|
||
customStyles,
|
||
fonts,
|
||
hasTailwindCDN: htmlContent.includes('cdn.tailwindcss.com')
|
||
};
|
||
}
|
||
|
||
const viteConfigPath = path.join(targetDir, 'vite.config.ts');
|
||
if (fs.existsSync(viteConfigPath)) {
|
||
const viteContent = fs.readFileSync(viteConfigPath, 'utf8');
|
||
|
||
const aliasMatch = viteContent.match(/alias:\s*{([^}]*)}/);
|
||
const aliases = aliasMatch ? aliasMatch[1].trim() : null;
|
||
|
||
const defineMatch = viteContent.match(/define:\s*{([^}]*)}/);
|
||
const envVars = defineMatch ? defineMatch[1].trim() : null;
|
||
|
||
analysis.viteConfig = {
|
||
hasAlias: !!aliases,
|
||
aliases,
|
||
hasEnvVars: !!envVars,
|
||
envVars
|
||
};
|
||
}
|
||
|
||
const packageJsonPath = path.join(targetDir, 'package.json');
|
||
if (fs.existsSync(packageJsonPath)) {
|
||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||
const deps = packageJson.dependencies || {};
|
||
analysis.dependencies = {
|
||
all: deps,
|
||
toInstall: Object.keys(deps).filter(dep => {
|
||
if (dep === 'react' || dep === 'react-dom') return false;
|
||
return true;
|
||
}),
|
||
excluded: ['react', 'react-dom']
|
||
};
|
||
} else if (analysis.indexHtml?.importMap) {
|
||
const imports = analysis.indexHtml.importMap.imports || {};
|
||
const cdnDeps = Object.keys(imports).filter(dep => {
|
||
if (dep === 'react' || dep === 'react-dom') return false;
|
||
return true;
|
||
});
|
||
|
||
analysis.dependencies = {
|
||
fromCDN: cdnDeps,
|
||
toInstall: cdnDeps,
|
||
excluded: ['react', 'react-dom']
|
||
};
|
||
}
|
||
|
||
analysis.structure = {
|
||
hasAppTsx: fs.existsSync(path.join(targetDir, 'App.tsx')),
|
||
hasIndexTsx: fs.existsSync(path.join(targetDir, 'index.tsx')),
|
||
hasIndexHtml: fs.existsSync(path.join(targetDir, 'index.html')),
|
||
hasComponentsDir: fs.existsSync(path.join(targetDir, 'components')),
|
||
hasAssetsDir: fs.existsSync(path.join(targetDir, 'assets')),
|
||
hasConstantsTs: fs.existsSync(path.join(targetDir, 'constants.ts')),
|
||
hasTypesTs: fs.existsSync(path.join(targetDir, 'types.ts')),
|
||
hasViteConfig: fs.existsSync(path.join(targetDir, 'vite.config.ts')),
|
||
hasMetadataJson: fs.existsSync(path.join(targetDir, 'metadata.json'))
|
||
};
|
||
|
||
return analysis;
|
||
}
|
||
|
||
function buildAnalysisReport(analysis) {
|
||
return {
|
||
summary: {
|
||
totalFiles: analysis.files.length,
|
||
componentCount: analysis.components.length,
|
||
dependenciesToInstall: analysis.dependencies.toInstall?.length || 0,
|
||
hasImportMap: !!analysis.indexHtml?.importMap,
|
||
hasCustomStyles: (analysis.indexHtml?.customStyles?.length || 0) > 0,
|
||
hasFonts: (analysis.indexHtml?.fonts?.length || 0) > 0
|
||
},
|
||
structure: analysis.structure,
|
||
components: analysis.components,
|
||
dependencies: analysis.dependencies,
|
||
indexHtml: analysis.indexHtml,
|
||
viteConfig: analysis.viteConfig,
|
||
files: analysis.files
|
||
};
|
||
}
|
||
|
||
function generateDefaultTasksDocument(report, targetInfo, outputName, tempDir) {
|
||
const reportPath = path.join(targetInfo.outputDir, targetInfo.analysisFileName);
|
||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
||
|
||
let markdown = `# AI Studio 项目转换任务清单\n\n`;
|
||
markdown += `> **重要**: 请先阅读 \`/skills/ai-studio-project-converter/SKILL.md\` 了解转换规范\n\n`;
|
||
markdown += `**名称**: ${outputName}\n`;
|
||
markdown += `**项目位置**: \`${targetInfo.relativeOutputDir}/\`\n`;
|
||
markdown += `**原始文件**: \`${tempDir}\` (仅供参考,不要修改)\n`;
|
||
markdown += `**生成时间**: ${new Date().toLocaleString()}\n\n`;
|
||
|
||
markdown += `## 📊 项目概况\n\n`;
|
||
markdown += `- 总文件数: ${report.summary.totalFiles}\n`;
|
||
markdown += `- 组件数: ${report.summary.componentCount}\n`;
|
||
markdown += `- Import Map: ${report.summary.hasImportMap ? '✓ 存在' : '✗ 不存在'}\n`;
|
||
markdown += `- 自定义样式: ${report.summary.hasCustomStyles ? '✓ 存在' : '✗ 不存在'}\n`;
|
||
markdown += `- 外部字体: ${report.summary.hasFonts ? '✓ 存在' : '✗ 不存在'}\n`;
|
||
markdown += `- 需要安装的依赖: ${report.summary.dependenciesToInstall} 个\n\n`;
|
||
|
||
markdown += `## ✅ 转换任务(共 5 个)\n\n`;
|
||
|
||
markdown += `### 任务 1: 转换主应用组件\n\n`;
|
||
markdown += `**目标**: 将 \`App.tsx\` 转换为本项目组件规范\n\n`;
|
||
if (report.structure.hasAppTsx) {
|
||
markdown += `**参考文件**: \`${targetInfo.relativeOutputDir}/App.tsx\`\n\n`;
|
||
markdown += `**操作**:\n`;
|
||
markdown += `1. 重命名 \`App.tsx\` 为 \`index.tsx\`\n`;
|
||
markdown += `2. 按照 \`/skills/ai-studio-project-converter/SKILL.md\` 中的本项目组件规范改造\n`;
|
||
markdown += `3. 添加文件头部注释(\`@name\` 和参考资料)\n`;
|
||
markdown += `4. 使用 \`forwardRef(function ComponentName(props: AxureProps, ref: React.ForwardedRef<AxureHandle>) { ... })\` 包装\n`;
|
||
markdown += `5. 实现 \`useImperativeHandle\`\n`;
|
||
markdown += `6. 保持原有的 JSX、Hooks 和 Tailwind 类名不变\n\n`;
|
||
} else {
|
||
markdown += `⚠️ 未找到 \`App.tsx\`,请手动创建 \`index.tsx\`\n\n`;
|
||
}
|
||
|
||
markdown += `### 任务 2: 创建 style.css\n\n`;
|
||
markdown += `**目标**: 提取 index.html 中的样式信息\n\n`;
|
||
if (report.indexHtml) {
|
||
markdown += `**操作**:\n`;
|
||
markdown += `1. 创建 \`style.css\`,开头添加 \`@import "tailwindcss";\`\n`;
|
||
|
||
if (report.indexHtml.customStyles.length > 0) {
|
||
markdown += `2. 从 \`index.html\` 的 \`<style>\` 标签提取自定义样式\n`;
|
||
markdown += `3. 将提取的样式添加到 \`style.css\`\n`;
|
||
}
|
||
|
||
if (report.indexHtml.fonts.length > 0) {
|
||
markdown += `4. 添加外部字体引用:\n`;
|
||
report.indexHtml.fonts.forEach(font => {
|
||
markdown += ` \`@import url('${font}');\`\n`;
|
||
});
|
||
}
|
||
markdown += `\n`;
|
||
} else {
|
||
markdown += `**操作**: 创建基础样式文件,内容为 \`@import "tailwindcss";\`\n\n`;
|
||
}
|
||
|
||
markdown += `### 任务 3: 移除 AI Studio 特定文件\n\n`;
|
||
markdown += `**目标**: 删除不需要的文件\n\n`;
|
||
markdown += `**需要删除**:\n`;
|
||
if (report.structure.hasIndexHtml) {
|
||
markdown += `- ✓ \`index.html\` (已提取信息)\n`;
|
||
}
|
||
if (report.structure.hasIndexTsx) {
|
||
markdown += `- ✓ \`index.tsx\` (本项目有自己的入口)\n`;
|
||
}
|
||
if (report.structure.hasViteConfig) {
|
||
markdown += `- ⚠️ \`vite.config.ts\` (检查路径别名后可删除)\n`;
|
||
}
|
||
if (report.structure.hasMetadataJson) {
|
||
markdown += `- ⚠️ \`metadata.json\` (可选保留作为参考)\n`;
|
||
}
|
||
markdown += `\n`;
|
||
|
||
markdown += `### 任务 4: 安装依赖\n\n`;
|
||
if (report.dependencies.toInstall && report.dependencies.toInstall.length > 0) {
|
||
markdown += `**执行命令**:\n`;
|
||
markdown += `\`\`\`bash\n`;
|
||
markdown += `pnpm add ${report.dependencies.toInstall.join(' ')}\n`;
|
||
markdown += `\`\`\`\n\n`;
|
||
|
||
if (report.dependencies.fromCDN) {
|
||
markdown += `**CDN 依赖映射**:\n`;
|
||
report.dependencies.fromCDN.forEach(dep => {
|
||
markdown += `- \`${dep}\` (从 Import Map 识别)\n`;
|
||
});
|
||
markdown += `\n`;
|
||
}
|
||
} else {
|
||
markdown += `✓ 无需安装额外依赖\n\n`;
|
||
}
|
||
|
||
if (report.viteConfig?.hasEnvVars) {
|
||
markdown += `**环境变量**:\n`;
|
||
markdown += `⚠️ 项目使用了环境变量,需要配置 \`.env.local\`\n`;
|
||
markdown += `\`\`\`\n${report.viteConfig.envVars}\n\`\`\`\n\n`;
|
||
}
|
||
|
||
markdown += `### 任务 5: 验收测试\n\n`;
|
||
markdown += `**执行命令**:\n`;
|
||
markdown += `\`\`\`bash\n`;
|
||
markdown += `node scripts/check-app-ready.mjs ${targetInfo.checkPath}\n`;
|
||
markdown += `\`\`\`\n\n`;
|
||
markdown += `**验收标准**: 状态为 READY,页面正常渲染,无控制台错误\n\n`;
|
||
|
||
markdown += `## 📚 参考资料\n\n`;
|
||
markdown += `- **转换规范**: \`/skills/ai-studio-project-converter/SKILL.md\`\n`;
|
||
markdown += `- **原始项目**: \`${tempDir}\` (仅供参考)\n`;
|
||
markdown += `- **详细数据**: \`${targetInfo.analysisFileName}\`\n\n`;
|
||
|
||
markdown += `## 💡 注意事项\n\n`;
|
||
markdown += `1. **Import Map**: CDN 依赖需转换为 npm 包\n`;
|
||
markdown += `2. **自定义样式**: 从 index.html 提取到 style.css\n`;
|
||
markdown += `3. **路径别名**: 检查 vite.config.ts 中的 alias 配置\n`;
|
||
markdown += `4. **环境变量**: \`process.env.*\` 需改为 \`import.meta.env.VITE_*\`\n`;
|
||
markdown += `5. **原始文件**: \`${tempDir}\` 目录保留作为参考,不要修改\n`;
|
||
markdown += `6. **验证**: 完成后务必运行验收脚本确认\n`;
|
||
|
||
const mdPath = path.join(targetInfo.outputDir, targetInfo.tasksFileName);
|
||
fs.writeFileSync(mdPath, markdown);
|
||
|
||
return { reportPath, mdPath };
|
||
}
|
||
|
||
function generateThemeTasksDocument(report, targetInfo, outputName, tempDir) {
|
||
const reportPath = path.join(targetInfo.outputDir, targetInfo.analysisFileName);
|
||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
||
|
||
let markdown = `# AI Studio 主题导入任务清单\n\n`;
|
||
markdown += `> **重要**: 请先阅读 \`/skills/ai-studio-project-converter/SKILL.md\` 与主题拆分技能文档,按任务顺序执行\n\n`;
|
||
markdown += `**主题 key**: ${outputName}\n`;
|
||
markdown += `**主题目录**: \`${targetInfo.relativeOutputDir}/\`\n`;
|
||
markdown += `**原始文件**: \`${tempDir}\` (仅供参考,不要修改)\n`;
|
||
markdown += `**生成时间**: ${new Date().toLocaleString()}\n\n`;
|
||
|
||
markdown += `## 📊 输入概况\n\n`;
|
||
markdown += `- 总文件数: ${report.summary.totalFiles}\n`;
|
||
markdown += `- 组件数: ${report.summary.componentCount}\n`;
|
||
markdown += `- Import Map: ${report.summary.hasImportMap ? '✓' : '✗'}\n`;
|
||
markdown += `- 自定义样式: ${report.summary.hasCustomStyles ? '✓' : '✗'}\n`;
|
||
markdown += `- 待评估依赖: ${report.summary.dependenciesToInstall} 个\n\n`;
|
||
|
||
markdown += `## 📚 参考文档(必须阅读)\n\n`;
|
||
markdown += `- \`/skills/ai-studio-project-converter/SKILL.md\`\n`;
|
||
THEME_SPLIT_SKILL_DOCS.forEach((docPath) => {
|
||
markdown += `- \`${docPath}\`\n`;
|
||
});
|
||
markdown += `\n`;
|
||
|
||
markdown += `## ✅ 主题导入任务(共 5 个)\n\n`;
|
||
|
||
markdown += `### 任务 1:生成主题 token\n\n`;
|
||
markdown += `**目标**:在 \`${targetInfo.relativeOutputDir}/\` 下生成 \`globals.css\` 或 \`designToken.json\`(二选一)\n\n`;
|
||
markdown += `**要求**:\n`;
|
||
markdown += `- 若输出 \`designToken.json\`,必须包含 \`name\` 字段\n`;
|
||
markdown += `- 结合 AI Studio 项目中的配色、字体和组件样式提炼 token\n\n`;
|
||
|
||
markdown += `### 任务 2:生成 DESIGN-SPEC.md\n\n`;
|
||
markdown += `**目标**:输出 \`${targetInfo.relativeOutputDir}/DESIGN-SPEC.md\`\n\n`;
|
||
markdown += `**要求**:说明视觉语言、组件规范、排版层级、交互状态与适配策略\n\n`;
|
||
|
||
markdown += `### 任务 3:按需生成项目文档\n\n`;
|
||
markdown += `**目标**:在 \`src/docs/\` 下补充主题相关文档(如页面地图、项目概览)\n\n`;
|
||
markdown += `**要求**:保证文档与主题规范和业务上下文一致\n\n`;
|
||
|
||
markdown += `### 任务 4:按需生成数据模型\n\n`;
|
||
markdown += `**目标**:在 \`src/database/\` 下补充或更新数据模型\n\n`;
|
||
markdown += `**要求**:\n`;
|
||
markdown += `- 文件名英文、\`tableName\` 中文\n`;
|
||
markdown += `- 每个表包含 \`records\` 数组,记录 id 唯一\n\n`;
|
||
|
||
markdown += `### 任务 5:生成/更新主题演示入口\n\n`;
|
||
markdown += `**目标**:生成或更新 \`${targetInfo.relativeOutputDir}/index.tsx\`\n\n`;
|
||
markdown += `**要求**:能清晰展示主题样式与 token 使用方式\n\n`;
|
||
|
||
markdown += `## 🔍 验收建议\n\n`;
|
||
markdown += `- 目录检查:\`${targetInfo.relativeOutputDir}/\` 是否包含 token 文件、\`DESIGN-SPEC.md\` 与 \`index.tsx\`\n`;
|
||
markdown += `- 文档检查:\`src/docs/\` 是否按需补充\n`;
|
||
markdown += `- 数据检查:\`src/database/\` JSON 结构是否满足约束\n\n`;
|
||
|
||
markdown += `## 📎 产物索引\n\n`;
|
||
markdown += `- 任务清单:\`${targetInfo.tasksFileName}\`\n`;
|
||
markdown += `- 分析报告:\`${targetInfo.analysisFileName}\`\n`;
|
||
|
||
const mdPath = path.join(targetInfo.outputDir, targetInfo.tasksFileName);
|
||
fs.writeFileSync(mdPath, markdown);
|
||
|
||
return { reportPath, mdPath };
|
||
}
|
||
|
||
function parseArgs(rawArgs) {
|
||
const args = [...rawArgs];
|
||
const help = args.length === 0 || args.includes('--help') || args.includes('-h');
|
||
|
||
if (help) {
|
||
return { help: true };
|
||
}
|
||
|
||
let projectDirArg = '';
|
||
let outputNameArg = '';
|
||
let targetType = 'prototypes';
|
||
|
||
for (let index = 0; index < args.length; index += 1) {
|
||
const arg = args[index];
|
||
if (arg === '--target-type') {
|
||
const nextValue = args[index + 1];
|
||
if (!nextValue) {
|
||
throw new Error('参数 --target-type 缺少值');
|
||
}
|
||
targetType = String(nextValue).trim();
|
||
index += 1;
|
||
continue;
|
||
}
|
||
|
||
if (!projectDirArg) {
|
||
projectDirArg = arg;
|
||
continue;
|
||
}
|
||
|
||
if (!outputNameArg) {
|
||
outputNameArg = arg;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
if (!projectDirArg) {
|
||
throw new Error('缺少 <ai-studio-project-dir> 参数');
|
||
}
|
||
|
||
if (!Object.prototype.hasOwnProperty.call(TARGET_TYPE_TO_SRC_DIR, targetType)) {
|
||
throw new Error(`不支持的 targetType: ${targetType}。可选值: ${Object.keys(TARGET_TYPE_TO_SRC_DIR).join(', ')}`);
|
||
}
|
||
|
||
const outputName = sanitizeName(outputNameArg || path.basename(projectDirArg));
|
||
if (!outputName) {
|
||
throw new Error('无法生成有效的输出名称,请显式传入 [output-name]');
|
||
}
|
||
|
||
return {
|
||
help: false,
|
||
projectDirArg,
|
||
outputName,
|
||
targetType,
|
||
};
|
||
}
|
||
|
||
function printHelp() {
|
||
console.log(`
|
||
AI Studio 项目预处理器
|
||
|
||
使用方法:
|
||
node scripts/ai-studio-converter.mjs <ai-studio-project-dir> [output-name] [--target-type <prototypes|components|themes>]
|
||
|
||
示例:
|
||
node scripts/ai-studio-converter.mjs "temp/my-ai-studio-project" my-page
|
||
node scripts/ai-studio-converter.mjs "temp/my-ai-studio-project" brand-theme --target-type themes
|
||
|
||
功能:
|
||
- 完整复制 AI Studio 项目(不修改代码)
|
||
- 生成 AI 工作文档(默认 .ai-studio-tasks.md;主题模式 .ai-studio-theme-tasks.md)
|
||
- 生成分析报告(默认 .ai-studio-analysis.json;主题模式 .ai-studio-theme-analysis.json)
|
||
`);
|
||
}
|
||
|
||
async function main() {
|
||
let parsed;
|
||
try {
|
||
parsed = parseArgs(process.argv.slice(2));
|
||
} catch (error) {
|
||
log(`参数错误: ${error.message}`, 'error');
|
||
printHelp();
|
||
process.exit(1);
|
||
}
|
||
|
||
if (parsed.help) {
|
||
printHelp();
|
||
process.exit(0);
|
||
}
|
||
|
||
const aiStudioDir = path.resolve(CONFIG.projectRoot, parsed.projectDirArg);
|
||
const targetInfo = getTargetInfo(parsed.targetType, parsed.outputName);
|
||
const outputDir = targetInfo.outputDir;
|
||
|
||
if (!fs.existsSync(aiStudioDir)) {
|
||
log(`错误: 找不到目录 ${aiStudioDir}`, 'error');
|
||
process.exit(1);
|
||
}
|
||
|
||
const appTsx = path.join(aiStudioDir, 'App.tsx');
|
||
const indexHtml = path.join(aiStudioDir, 'index.html');
|
||
if (!fs.existsSync(appTsx) && !fs.existsSync(indexHtml)) {
|
||
log('错误: 这不是一个有效的 AI Studio 项目(缺少 App.tsx 或 index.html)', 'error');
|
||
process.exit(1);
|
||
}
|
||
|
||
try {
|
||
ensureDir(targetInfo.outputBaseDir);
|
||
|
||
log(`开始预处理 AI Studio 项目(targetType=${parsed.targetType})...`, 'info');
|
||
|
||
log('步骤 1/4: 复制项目文件...', 'progress');
|
||
const fileCount = copyDirectory(aiStudioDir, outputDir);
|
||
log(`已复制 ${fileCount} 个文件`, 'info');
|
||
|
||
log('步骤 2/4: 复制 assets 到目标目录...', 'progress');
|
||
const assetsDir = path.join(aiStudioDir, 'assets');
|
||
const outputAssetsDir = path.join(outputDir, 'assets');
|
||
let assetCount = 0;
|
||
if (fs.existsSync(assetsDir)) {
|
||
assetCount = copyDirectory(assetsDir, outputAssetsDir);
|
||
log(`已复制 ${assetCount} 个资源文件到 ${targetInfo.relativeOutputDir}/assets/`, 'info');
|
||
} else {
|
||
log('未找到 assets 目录,跳过', 'info');
|
||
}
|
||
|
||
log('步骤 3/4: 分析项目结构...', 'progress');
|
||
const analysis = analyzeProject(outputDir);
|
||
log(`发现 ${analysis.components.length} 个组件`, 'info');
|
||
|
||
log('步骤 4/4: 生成任务文档...', 'progress');
|
||
const report = buildAnalysisReport(analysis);
|
||
const { reportPath, mdPath } = parsed.targetType === 'themes'
|
||
? generateThemeTasksDocument(report, targetInfo, parsed.outputName, `temp/${path.basename(aiStudioDir)}`)
|
||
: generateDefaultTasksDocument(report, targetInfo, parsed.outputName, `temp/${path.basename(aiStudioDir)}`);
|
||
|
||
log('✅ 预处理完成!', 'info');
|
||
log('', 'info');
|
||
log(`📁 ${targetInfo.label}位置: ${targetInfo.relativeOutputDir}/`, 'info');
|
||
log(`📋 AI 工作文档: ${path.relative(CONFIG.projectRoot, mdPath)}`, 'info');
|
||
log(`📊 详细数据: ${path.relative(CONFIG.projectRoot, reportPath)}`, 'info');
|
||
log('', 'info');
|
||
log('📈 统计:', 'info');
|
||
log(` - 文件数: ${analysis.files.length}`, 'info');
|
||
log(` - 组件数: ${analysis.components.length}`, 'info');
|
||
log(` - 依赖: ${analysis.dependencies.toInstall?.length || 0} 个`, 'info');
|
||
log('', 'info');
|
||
log('🎯 下一步:', 'info');
|
||
log(`1. 查看任务文档: cat ${path.relative(CONFIG.projectRoot, mdPath)}`, 'info');
|
||
log(parsed.targetType === 'themes'
|
||
? '2. 让 AI 按任务单完成主题/文档/数据生成'
|
||
: '2. 让 AI 根据任务清单完成转换', 'info');
|
||
} catch (error) {
|
||
log(`预处理失败: ${error.message}`, 'error');
|
||
console.error(error);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
main();
|