UNPKG

@bold-ai/create-product

Version:

CLI tool to create new products with Company Auth pre-configured

272 lines (251 loc) 9.51 kB
// src/create-product.ts import fs from "fs-extra"; import path from "path"; import { fileURLToPath } from "url"; import { exec } from "child_process"; import { promisify } from "util"; import { nanoid } from "nanoid"; import os from "os"; import ora from "ora"; import chalk from "chalk"; var execAsync = promisify(exec); var __filename = fileURLToPath(import.meta.url); var __dirname = path.dirname(__filename); var GITHUB_REPO = "Bold-AI-Inc/bold-os-re"; var GITHUB_BRANCH = "main"; async function downloadTemplate(githubToken) { const tempDir = path.join(os.tmpdir(), `bold-template-${nanoid(8)}`); await fs.mkdir(tempDir, { recursive: true }); const apiUrl = `https://api.github.com/repos/${GITHUB_REPO}/contents/packages/product-template?ref=${GITHUB_BRANCH}`; const headers = { "Accept": "application/vnd.github.v3+json", "User-Agent": "bold-create-product-cli" }; if (githubToken) { headers["Authorization"] = `token ${githubToken}`; } const response = await fetch(apiUrl, { headers }); if (!response.ok) { if (response.status === 401) { throw new Error("Invalid GitHub token"); } if (response.status === 403) { throw new Error("GitHub API rate limit exceeded. Please provide a GitHub token or try again later."); } if (response.status === 404) { throw new Error("Template not found in repository"); } throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); } const contents = await response.json(); if (!Array.isArray(contents)) { throw new Error("Unexpected response from GitHub API"); } await downloadContents(contents, tempDir, githubToken); return tempDir; } async function downloadContents(contents, targetPath, githubToken) { for (const item of contents) { const itemPath = path.join(targetPath, item.name); if (["node_modules", ".next", "dist", ".turbo"].includes(item.name)) { continue; } if (item.type === "file") { const headers = { "User-Agent": "bold-create-product-cli" }; if (githubToken) { headers["Authorization"] = `token ${githubToken}`; } const fileResponse = await fetch(item.download_url, { headers }); if (!fileResponse.ok) { throw new Error(`Failed to download ${item.name}`); } const fileContent = await fileResponse.text(); await fs.writeFile(itemPath, fileContent, "utf-8"); } else if (item.type === "dir") { await fs.mkdir(itemPath, { recursive: true }); const headers = { "Accept": "application/vnd.github.v3+json", "User-Agent": "bold-create-product-cli" }; if (githubToken) { headers["Authorization"] = `token ${githubToken}`; } const dirResponse = await fetch(item.url, { headers }); if (!dirResponse.ok) { throw new Error(`Failed to fetch directory ${item.name}`); } const dirContents = await dirResponse.json(); await downloadContents(dirContents, itemPath, githubToken); } } } async function createProduct(options) { const { projectName, displayName, description, authServiceUrl, supabaseUrl, supabaseAnonKey, skipInstall, githubToken } = options; const workspaceRoot = path.join(process.cwd(), projectName); const appDir = path.join(workspaceRoot, "apps", projectName); const packagesDir = path.join(workspaceRoot, "packages"); if (await fs.pathExists(workspaceRoot)) { throw new Error(`Directory ${projectName} already exists`); } let templateDir = path.resolve(__dirname, "../../../product-template"); let downloadedTemplate = false; if (!await fs.pathExists(templateDir)) { const spinner = ora("Downloading template from GitHub...").start(); try { templateDir = await downloadTemplate(githubToken); downloadedTemplate = true; spinner.succeed("Template downloaded"); } catch (error) { spinner.fail("Failed to download template"); throw error; } } await fs.mkdirp(appDir); await fs.mkdirp(packagesDir); await fs.copy(templateDir, appDir, { filter: (src) => { const relativePath = path.relative(templateDir, src); return !relativePath.includes("node_modules") && !relativePath.includes(".next") && !relativePath.includes("dist") && !relativePath.includes(".turbo"); } }); const packageJsonPath = path.join(appDir, "package.json"); const packageJson = await fs.readJson(packageJsonPath); packageJson.name = projectName; packageJson.version = "0.1.0"; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); const envExamplePath = path.join(appDir, ".env.example"); const envLocalPath = path.join(appDir, ".env.local"); let envContent = await fs.readFile(envExamplePath, "utf-8"); const cleanSupabaseUrl = supabaseUrl?.replace(/^https?:\/\//, "") || "your-project.supabase.co"; const cleanAuthServiceUrl = authServiceUrl?.replace(/^https?:\/\//, "") || "your-auth-service.vercel.app"; envContent = envContent.replace("your-project.supabase.co", cleanSupabaseUrl).replace(/your-anon-key-here/g, supabaseAnonKey || "your-anon-key-here").replace("your-auth-service.vercel.app", cleanAuthServiceUrl).replace("your-product-id-here", "PENDING_REGISTRATION").replace("My Product", displayName).replace("sk-test-dummy-key", "sk-test-dummy-key # Replace with your actual OpenAI key"); if (!envContent.includes("AGENCY_AUTH_API_KEY=")) { envContent = envContent.replace( /AGENCY_AUTH_PRODUCT_ID=.*/, `AGENCY_AUTH_PRODUCT_ID=PENDING_REGISTRATION # API Key from Agency Auth Service (keep this secret!) AGENCY_AUTH_API_KEY=PENDING_REGISTRATION` ); } await fs.writeFile(envLocalPath, envContent); const readmePath = path.join(appDir, "README.md"); let readmeContent = await fs.readFile(readmePath, "utf-8"); readmeContent = readmeContent.replace("Company Product Template", displayName); readmeContent = `# ${displayName} ${description} ` + readmeContent.split("\n").slice(2).join("\n"); await fs.writeFile(readmePath, readmeContent); const workspaceYaml = `packages: - 'apps/*' - 'packages/*' `; await fs.writeFile(path.join(workspaceRoot, "pnpm-workspace.yaml"), workspaceYaml); const rootPackageJson = { name: `${projectName}-workspace`, version: "0.1.0", private: true, scripts: { dev: `pnpm build:packages && pnpm --filter ${projectName} dev`, build: `pnpm build:packages && pnpm --filter ${projectName} build`, "build:packages": `pnpm --filter '@bold/*' build`, "dev:packages": `pnpm --filter '@bold/*' dev`, lint: `pnpm --filter ${projectName} lint`, start: `pnpm --filter ${projectName} start` }, engines: { node: ">=18.0.0", pnpm: ">=8.0.0" } }; await fs.writeJson(path.join(workspaceRoot, "package.json"), rootPackageJson, { spaces: 2 }); const rootReadme = `# ${displayName} ${description} This is a pnpm workspace containing your Bold product. ## Structure \`\`\` ${projectName}/ \u251C\u2500\u2500 pnpm-workspace.yaml # Workspace configuration \u251C\u2500\u2500 package.json # Workspace root \u251C\u2500\u2500 apps/ \u2502 \u2514\u2500\u2500 ${projectName}/ # Your Next.js application \u2514\u2500\u2500 packages/ # Bold packages (agents, auth-sdk, logger, subscriptions) \`\`\` ## Quick Start \`\`\`bash # Install all dependencies pnpm install # Build Bold packages (required first time) pnpm build:packages # Run your app in development mode pnpm dev # Build your app for production pnpm build \`\`\` ## Important: OpenAI API Key for AI Agents If you want to use the \`@bold/agents\` package, you need to add your OpenAI API key: 1. Get your API key from [OpenAI Platform](https://platform.openai.com/api-keys) 2. Add it to \`apps/${projectName}/.env.local\`: \`\`\` OPENAI_API_KEY=sk-your-actual-key-here \`\`\` ## Available Scripts - \`pnpm dev\` - Build packages and start dev server - \`pnpm build\` - Build packages and create production build - \`pnpm build:packages\` - Build all @bold/* packages - \`pnpm dev:packages\` - Watch and rebuild packages on changes - \`pnpm lint\` - Run linter on your app - \`pnpm start\` - Start production server ## Learn More - [Bold Documentation](https://github.com/Bold-AI-Inc/bold-os-re) - [pnpm Workspaces](https://pnpm.io/workspaces) - [OpenAI API Keys](https://platform.openai.com/api-keys) `; await fs.writeFile(path.join(workspaceRoot, "README.md"), rootReadme); if (downloadedTemplate) { try { await fs.remove(templateDir); } catch (error) { console.warn(chalk.dim("Note: Could not clean up temporary template directory")); } } return { projectName, workspaceRoot, appDir, packagesDir }; } async function updateEnvWithCredentials(appDir, productId, apiKey, adminApiKey) { const envLocalPath = path.join(appDir, ".env.local"); let envContent = await fs.readFile(envLocalPath, "utf-8"); envContent = envContent.replace( /NEXT_PUBLIC_AGENCY_AUTH_PRODUCT_ID=.*/, `NEXT_PUBLIC_AGENCY_AUTH_PRODUCT_ID=${productId}` ); envContent = envContent.replace( /NEXT_PUBLIC_AGENCY_AUTH_API_KEY=.*/, `NEXT_PUBLIC_AGENCY_AUTH_API_KEY=${apiKey}` ); if (adminApiKey) { envContent = envContent.replace( /ADMIN_API_KEY=.*/, `ADMIN_API_KEY=${adminApiKey}` ); } await fs.writeFile(envLocalPath, envContent); } export { createProduct, updateEnvWithCredentials };