@bold-ai/create-product
Version:
CLI tool to create new products with Company Auth pre-configured
272 lines (251 loc) • 9.51 kB
JavaScript
// 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
};