567 lines
21 KiB
TypeScript
567 lines
21 KiB
TypeScript
import chalk from 'chalk';
|
|
import fs from 'fs-extra';
|
|
import path from 'path';
|
|
import inquirer from 'inquirer';
|
|
import axios from 'axios';
|
|
import AdmZip from 'adm-zip';
|
|
import { getAuthHeaders } from './utils/webLogin';
|
|
import { startBackgroundInstall } from './utils/installProjectDependencies';
|
|
import {
|
|
createApiError,
|
|
createCommandError,
|
|
createConfigError,
|
|
printCliError,
|
|
} from './utils/cliError';
|
|
import { APP_CONFIG, getPinmeApiUrl } from './utils/config';
|
|
import { uploadPath } from './services/uploadService';
|
|
import { printHighlightedUrl } from './utils/urlDisplay';
|
|
import { patchPrebuiltFrontendDist } from './utils/prebuiltDistConfig';
|
|
import { getValidatedWorkerMetadataContent } from './utils/workerMetadata';
|
|
import { downloadFileWithRetries, getDownloadErrorMessage } from './utils/downloadFile';
|
|
import tracker, { getTrackErrorReason } from './utils/tracker';
|
|
import {
|
|
TRACK_EVENTS,
|
|
TRACK_PAGES,
|
|
resolveTrackAction,
|
|
} from './utils/trackerEvents';
|
|
|
|
// Template directory - relative to bin folder (works both in dev and npm)
|
|
const PROJECT_DIR = process.cwd();
|
|
const TEMPLATE_BRANCH = process.env.PINME_TEMPLATE_BRANCH || 'main';
|
|
|
|
// 模板仓库地址 (使用 HTTPS 下载 zip)
|
|
const TEMPLATE_REPO = 'glitternetwork/pinme-worker-template';
|
|
const TEMPLATE_REPO_NAME = TEMPLATE_REPO.split('/').pop() || 'pinme-worker-template';
|
|
|
|
function getTemplateZipUrl(branch: string): string {
|
|
return `https://github.com/${TEMPLATE_REPO}/archive/refs/heads/${encodeURIComponent(branch)}.zip`;
|
|
}
|
|
|
|
interface CreateOptions {
|
|
name?: string;
|
|
force?: boolean;
|
|
}
|
|
|
|
interface CreateWorkerResponse {
|
|
api_domain: string;
|
|
metadata: string;
|
|
project_name: string;
|
|
uuid: string;
|
|
api_key?: string;
|
|
public_client_config?: Record<string, any>;
|
|
}
|
|
|
|
function buildPublicClientConfigExport(publicClientConfig: Record<string, any>): string {
|
|
return `export const public_client_config = ${JSON.stringify(publicClientConfig, null, 2)};\n`;
|
|
}
|
|
|
|
function injectPublicClientConfigIntoFile(fileContent: string, publicClientConfig: Record<string, any>): string {
|
|
const configExport = buildPublicClientConfigExport(publicClientConfig).trimEnd();
|
|
const configPattern = /export\s+const\s+public_client_config\s*=\s*\{[\s\S]*?\};?/m;
|
|
|
|
if (configPattern.test(fileContent)) {
|
|
return fileContent.replace(configPattern, configExport);
|
|
}
|
|
|
|
const trimmed = fileContent.trimEnd();
|
|
if (!trimmed) {
|
|
return `${configExport}\n`;
|
|
}
|
|
|
|
return `${trimmed}\n\n${configExport}\n`;
|
|
}
|
|
|
|
function resolveExtractedTemplateDir(extractDir: string): string {
|
|
const entries = fs.readdirSync(extractDir, { withFileTypes: true });
|
|
const templateDir = entries.find((entry) => (
|
|
entry.isDirectory() && entry.name.startsWith(`${TEMPLATE_REPO_NAME}-`)
|
|
));
|
|
|
|
if (!templateDir) {
|
|
throw createConfigError('Downloaded template archive structure is invalid.', [
|
|
`Expected a directory starting with \`${TEMPLATE_REPO_NAME}-\` inside the extracted archive.`,
|
|
`Template branch: ${TEMPLATE_BRANCH}`,
|
|
]);
|
|
}
|
|
|
|
return path.join(extractDir, templateDir.name);
|
|
}
|
|
|
|
function updateFrontendUrlInConfig(configPath: string, frontendUrl: string): void {
|
|
let config = fs.readFileSync(configPath, 'utf-8');
|
|
|
|
if (config.includes('frontend_url')) {
|
|
config = config.replace(
|
|
/frontend_url\s*=\s*"[^"]*"/,
|
|
`frontend_url = "${frontendUrl}"`,
|
|
);
|
|
} else {
|
|
config = config.replace(
|
|
/(project_name\s*=\s*"[^"]*"\n)/,
|
|
`$1frontend_url = "${frontendUrl}"\n`,
|
|
);
|
|
}
|
|
|
|
fs.writeFileSync(configPath, config);
|
|
}
|
|
|
|
function getProjectManagementUrl(projectName: string): string {
|
|
return `${APP_CONFIG.projectPeviewUrl}${projectName}`;
|
|
}
|
|
|
|
/**
|
|
* Create a new project from template
|
|
* 1. Check login
|
|
* 2. Call API to create worker/D1 database
|
|
* 3. Copy template files
|
|
* 4. Update configuration files
|
|
*/
|
|
export default async function createCmd(options: CreateOptions): Promise<void> {
|
|
try {
|
|
// Check if user is logged in
|
|
const headers = getAuthHeaders();
|
|
if (!headers['authentication-tokens'] || !headers['token-address']) {
|
|
throw createConfigError('No valid local login session was found.', [
|
|
'Run `pinme login` and retry.',
|
|
]);
|
|
}
|
|
|
|
console.log(chalk.blue('Creating new project from template...\n'));
|
|
|
|
// Get project name from options or prompt
|
|
let projectName = options.name;
|
|
if (!projectName) {
|
|
const answers = await inquirer.prompt([
|
|
{
|
|
type: 'input',
|
|
name: 'projectName',
|
|
message: 'Enter project name:',
|
|
validate: (input: string) => {
|
|
if (!input.trim()) return 'Project name is required';
|
|
if (!/^[a-zA-Z0-9-_]+$/.test(input)) {
|
|
return 'Project name can only contain letters, numbers, hyphens and underscores';
|
|
}
|
|
return true;
|
|
},
|
|
},
|
|
]);
|
|
projectName = answers.projectName;
|
|
}
|
|
|
|
const targetDir = path.join(PROJECT_DIR, projectName);
|
|
|
|
// Check if directory exists
|
|
if (fs.existsSync(targetDir) && !options.force) {
|
|
console.log(chalk.yellow(`\nDirectory "${projectName}" already exists.`));
|
|
const answers = await inquirer.prompt([
|
|
{
|
|
type: 'confirm',
|
|
name: 'overwrite',
|
|
message: 'Do you want to overwrite it?',
|
|
default: false,
|
|
},
|
|
]);
|
|
if (!answers.overwrite) {
|
|
console.log(chalk.gray('Cancelled.'));
|
|
process.exit(0);
|
|
}
|
|
fs.removeSync(targetDir);
|
|
}
|
|
|
|
// 1. Call API to create worker/D1
|
|
console.log(chalk.blue('\n1. Creating worker and database...'));
|
|
const apiUrl = getPinmeApiUrl('/create_worker');
|
|
console.log(chalk.gray(`API URL: ${apiUrl}`));
|
|
|
|
// Convert project name to lowercase (API requires lowercase)
|
|
const normalizedProjectName = projectName.toLowerCase();
|
|
console.log(chalk.gray(`Project name: ${normalizedProjectName}`));
|
|
|
|
let workerData: CreateWorkerResponse;
|
|
try {
|
|
const response = await axios.post(apiUrl, {
|
|
project_name: normalizedProjectName,
|
|
}, {
|
|
headers: {
|
|
...headers,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
const data = response.data;
|
|
|
|
if (data.code !== 200) {
|
|
throw createApiError('project creation', { response: { status: response.status, data } }, [
|
|
`Project name: ${normalizedProjectName}`,
|
|
`Endpoint: ${apiUrl}`,
|
|
]);
|
|
}
|
|
|
|
workerData = data.data;
|
|
console.log(chalk.gray(` API Response: ${JSON.stringify(workerData)}`));
|
|
console.log(chalk.green(` API Domain: ${workerData.api_domain}`));
|
|
console.log(chalk.green(` Project Name: ${workerData.project_name}`));
|
|
} catch (error: any) {
|
|
throw createApiError('project creation', error, [
|
|
`Project name: ${normalizedProjectName}`,
|
|
`Endpoint: ${apiUrl}`,
|
|
]);
|
|
}
|
|
|
|
// 2. Download template from repository (using HTTPS, no git required)
|
|
console.log(chalk.blue('\n2. Downloading template from repository...'));
|
|
const zipPath = path.join(PROJECT_DIR, 'template.zip');
|
|
const extractDir = path.join(PROJECT_DIR, `.pinme-template-${Date.now()}`);
|
|
const templateZipUrl =
|
|
process.env.PINME_TEMPLATE_ZIP_URL || getTemplateZipUrl(TEMPLATE_BRANCH);
|
|
console.log(chalk.gray(` Template branch: ${TEMPLATE_BRANCH}`));
|
|
|
|
try {
|
|
const downloadResult = await downloadFileWithRetries(templateZipUrl, zipPath, {
|
|
attempts: 3,
|
|
retryDelayMs: 2000,
|
|
minBytes: 100,
|
|
onAttempt: (attempt, attempts) => {
|
|
console.log(chalk.gray(` Download attempt ${attempt}/${attempts}...`));
|
|
},
|
|
onAttemptFailure: (attempt, error) => {
|
|
console.log(chalk.yellow(` Attempt ${attempt} failed: ${getDownloadErrorMessage(error)}`));
|
|
},
|
|
});
|
|
console.log(chalk.green(` Template archive downloaded (${downloadResult.bytes} bytes)`));
|
|
} catch (error: any) {
|
|
throw createCommandError('template download', `download "${templateZipUrl}" to "${zipPath}"`, error, [
|
|
'Check your network connection and retry `pinme create`.',
|
|
`Verify that the template branch exists: ${TEMPLATE_BRANCH}`,
|
|
]);
|
|
}
|
|
|
|
try {
|
|
fs.ensureDirSync(extractDir);
|
|
|
|
// Extract with Node so Windows users do not need a system `unzip` command.
|
|
const templateZip = new AdmZip(zipPath);
|
|
templateZip.extractAllTo(extractDir, true);
|
|
|
|
// Move files from extracted subdirectory to target directory
|
|
const subDir = resolveExtractedTemplateDir(extractDir);
|
|
fs.copySync(subDir, targetDir);
|
|
|
|
// Clean up zip file
|
|
fs.removeSync(zipPath);
|
|
fs.removeSync(extractDir);
|
|
|
|
// Remove any existing node_modules to avoid platform-specific dependency leftovers.
|
|
// This fixes issues with rollup platform-specific dependencies
|
|
const nodeModulesPath = path.join(targetDir, 'node_modules');
|
|
if (fs.existsSync(nodeModulesPath)) {
|
|
console.log(chalk.gray(' Removing existing node_modules...'));
|
|
fs.removeSync(nodeModulesPath);
|
|
}
|
|
// Also clean frontend and backend subdirectories
|
|
const frontendNodeModules = path.join(targetDir, 'frontend', 'node_modules');
|
|
const backendNodeModules = path.join(targetDir, 'backend', 'node_modules');
|
|
if (fs.existsSync(frontendNodeModules)) fs.removeSync(frontendNodeModules);
|
|
if (fs.existsSync(backendNodeModules)) fs.removeSync(backendNodeModules);
|
|
|
|
console.log(chalk.green(` Template downloaded to: ${targetDir}`));
|
|
} catch (error: any) {
|
|
throw createCommandError('template extraction', `extract "${zipPath}" to "${extractDir}"`, error, [
|
|
'Check whether the downloaded template archive is valid and has the expected GitHub archive structure.',
|
|
]);
|
|
}
|
|
|
|
// 3. Update pinme.toml with project_name
|
|
console.log(chalk.blue('\n3. Updating configuration...'));
|
|
const configPath = path.join(targetDir, 'pinme.toml');
|
|
const config = fs.readFileSync(configPath, 'utf-8');
|
|
|
|
// Update project_name
|
|
let updatedConfig = config.replace(
|
|
/project_name = ".*"/,
|
|
`project_name = "${workerData.project_name}"`
|
|
);
|
|
|
|
fs.writeFileSync(configPath, updatedConfig);
|
|
console.log(chalk.green(` Updated pinme.toml`));
|
|
console.log(chalk.gray(` metadata: ${workerData.metadata}`));
|
|
const workerMetadataContent = getValidatedWorkerMetadataContent(
|
|
workerData.metadata,
|
|
workerData.project_name,
|
|
);
|
|
|
|
// 4. Save metadata to backend directory
|
|
const backendDir = path.join(targetDir, 'backend');
|
|
if (fs.existsSync(backendDir)) {
|
|
fs.writeFileSync(
|
|
path.join(backendDir, 'metadata.json'),
|
|
workerMetadataContent
|
|
);
|
|
console.log(chalk.green(` Saved metadata.json`));
|
|
}
|
|
|
|
// 4.1 Update API_KEY in backend/wrangler.toml
|
|
const wranglerPath = path.join(backendDir, 'wrangler.toml');
|
|
if (fs.existsSync(wranglerPath) && workerData.api_key) {
|
|
let wranglerContent = fs.readFileSync(wranglerPath, 'utf-8');
|
|
wranglerContent = wranglerContent.replace(
|
|
/^name = ".*"$/m,
|
|
`name = "${workerData.project_name}"`
|
|
);
|
|
fs.writeFileSync(wranglerPath, wranglerContent);
|
|
console.log(chalk.green(` Updated backend/wrangler.toml API_KEY`));
|
|
}
|
|
|
|
const frontendConfigPath = path.join(targetDir, 'frontend', 'src', 'utils', 'config.ts');
|
|
if (workerData.public_client_config) {
|
|
const frontendConfigContent = fs.existsSync(frontendConfigPath)
|
|
? fs.readFileSync(frontendConfigPath, 'utf-8')
|
|
: '';
|
|
fs.ensureDirSync(path.dirname(frontendConfigPath));
|
|
fs.writeFileSync(
|
|
frontendConfigPath,
|
|
injectPublicClientConfigIntoFile(frontendConfigContent, workerData.public_client_config)
|
|
);
|
|
console.log(chalk.green(` Updated frontend/src/utils/config.ts public_client_config`));
|
|
}
|
|
|
|
|
|
// 5. Create .env file from .env.example (in frontend directory)
|
|
const envExamplePath = path.join(targetDir, 'frontend', '.env.example');
|
|
const envPath = path.join(targetDir, 'frontend', '.env');
|
|
if (fs.existsSync(envExamplePath)) {
|
|
let envContent = fs.readFileSync(envExamplePath, 'utf-8');
|
|
// Replace your-project with actual project name
|
|
envContent = envContent.replace(/your-project/g, workerData.project_name);
|
|
// Update API_URL - match entire line
|
|
envContent = envContent.replace(
|
|
/^VITE_API_URL=.*$/m,
|
|
`VITE_API_URL=${workerData.api_domain}`
|
|
);
|
|
fs.writeFileSync(envPath, envContent);
|
|
console.log(chalk.green(` Created frontend/.env file`));
|
|
console.log(chalk.gray(` VITE_API_URL: ${workerData.api_domain}`));
|
|
}
|
|
|
|
// Update pinme.toml with api_url (append after project_name)
|
|
let pinmeConfig = fs.readFileSync(configPath, 'utf-8');
|
|
if (pinmeConfig.includes('api_url')) {
|
|
pinmeConfig = pinmeConfig.replace(
|
|
/api_url\s*=\s*"[^"]*"/,
|
|
`api_url = "${workerData.api_domain}"`
|
|
);
|
|
} else {
|
|
// Append api_url line after project_name
|
|
const lines = pinmeConfig.split('\n');
|
|
const newLines: string[] = [];
|
|
for (const line of lines) {
|
|
newLines.push(line);
|
|
if (line.trim().startsWith('project_name')) {
|
|
newLines.push(`api_url = "${workerData.api_domain}"`);
|
|
}
|
|
}
|
|
pinmeConfig = newLines.join('\n');
|
|
}
|
|
fs.writeFileSync(configPath, pinmeConfig);
|
|
console.log(chalk.green(` Updated pinme.toml with api_url`));
|
|
|
|
// 6. Install dependencies in the background.
|
|
// The template ships prebuilt `dist-worker/` and `frontend/dist/`, so the
|
|
// initial create + deploy below does NOT need node_modules. We kick off the
|
|
// install asynchronously so dependencies are ready for later `pinme save`
|
|
// and local development, without blocking (or failing) the first create.
|
|
console.log(chalk.blue('\n4. Installing dependencies in the background...'));
|
|
try {
|
|
const { logPath } = startBackgroundInstall(targetDir);
|
|
console.log(chalk.gray(' Dependencies are installing in the background; create will continue.'));
|
|
console.log(chalk.gray(` Install progress is logged to: ${logPath}`));
|
|
console.log(chalk.gray(' `pinme save` will automatically wait for this install to finish.'));
|
|
} catch (error: any) {
|
|
// A failed background install must not block create; the prebuilt dist is enough.
|
|
console.log(chalk.yellow(' Warning: could not start background dependency install.'));
|
|
console.log(chalk.yellow(' Run `npm install` inside the project before `pinme save`.'));
|
|
}
|
|
|
|
// 7. Use the prebuilt backend worker shipped with the template
|
|
console.log(chalk.blue('\n5. Preparing backend worker...'));
|
|
|
|
// Get prebuilt worker files and SQL files
|
|
const distWorkerDir = path.join(targetDir, 'dist-worker');
|
|
const workerJsPath = path.join(distWorkerDir, 'worker.js');
|
|
|
|
if (!fs.existsSync(distWorkerDir) || !fs.existsSync(workerJsPath)) {
|
|
throw createConfigError('Prebuilt worker output not found: `dist-worker/worker.js`.', [
|
|
'The template should ship a prebuilt `dist-worker/`.',
|
|
'Once dependencies finish installing, run `npm run build:worker` in the project, then `pinme save`.',
|
|
]);
|
|
}
|
|
|
|
// Get module files
|
|
const modulePaths: string[] = [];
|
|
const files = fs.readdirSync(distWorkerDir);
|
|
for (const file of files) {
|
|
if (file.endsWith('.js') && file !== 'worker.js') {
|
|
modulePaths.push(path.join(distWorkerDir, file));
|
|
}
|
|
}
|
|
|
|
// Get SQL files
|
|
const sqlDir = path.join(targetDir, 'db');
|
|
const sqlFiles: string[] = [];
|
|
if (fs.existsSync(sqlDir)) {
|
|
const sqlFileNames = fs.readdirSync(sqlDir)
|
|
.filter(f => f.endsWith('.sql'))
|
|
.sort();
|
|
for (const filename of sqlFileNames) {
|
|
sqlFiles.push(path.join(sqlDir, filename));
|
|
console.log(chalk.gray(` Including SQL: ${filename}`));
|
|
}
|
|
}
|
|
|
|
// 9. Save worker to platform
|
|
console.log(chalk.blue('\n6. Deploying backend worker...'));
|
|
const saveApiUrl = `${getPinmeApiUrl('/save_worker')}?project_name=${encodeURIComponent(workerData.project_name)}`;
|
|
console.log(chalk.gray(` API URL: ${saveApiUrl}`));
|
|
|
|
try {
|
|
const FormData = (await import('formdata-node')).FormData;
|
|
const Blob = (await import('formdata-node')).Blob;
|
|
const formData = new FormData() as any;
|
|
|
|
// Add metadata
|
|
formData.append('metadata', new Blob([workerMetadataContent], {
|
|
type: 'application/json',
|
|
}), 'metadata.json');
|
|
|
|
// Add worker.js
|
|
const workerCode = fs.readFileSync(workerJsPath, 'utf-8');
|
|
formData.append('worker.js', new Blob([workerCode], {
|
|
type: 'application/javascript+module',
|
|
}), 'worker.js');
|
|
|
|
// Add other modules
|
|
for (const modulePath of modulePaths) {
|
|
const filename = path.basename(modulePath);
|
|
const content = fs.readFileSync(modulePath, 'utf-8');
|
|
formData.append(filename, new Blob([content], {
|
|
type: 'application/javascript+module',
|
|
}), filename);
|
|
}
|
|
|
|
// Add SQL files
|
|
for (const sqlFile of sqlFiles) {
|
|
const filename = path.basename(sqlFile);
|
|
const content = fs.readFileSync(sqlFile, 'utf-8');
|
|
formData.append('sql_file', new Blob([content], {
|
|
type: 'application/sql',
|
|
}), filename);
|
|
}
|
|
|
|
const response = await axios.put(saveApiUrl, formData, {
|
|
headers: { ...headers },
|
|
timeout: 120000,
|
|
});
|
|
|
|
if (response.data) {
|
|
console.log(chalk.green(' Worker deployed'));
|
|
if (response.data?.data?.sql_results) {
|
|
for (const result of response.data.data.sql_results) {
|
|
console.log(chalk.gray(` SQL ${result.filename}: ${result.status}`));
|
|
}
|
|
}
|
|
} else {
|
|
throw createApiError('worker save', { response: { data: response.data } }, [
|
|
`Project: ${workerData.project_name}`,
|
|
`Endpoint: ${saveApiUrl}`,
|
|
], [
|
|
'Verify the project exists and your account has permission to update it.',
|
|
]);
|
|
}
|
|
} catch (error: any) {
|
|
throw createApiError('worker save', error, [
|
|
`Project: ${workerData.project_name}`,
|
|
`Endpoint: ${saveApiUrl}`,
|
|
], [
|
|
'Check whether backend metadata, SQL files, or worker bundle contains invalid content.',
|
|
]);
|
|
}
|
|
|
|
// 10. Deploy the prebuilt frontend shipped with the template
|
|
console.log(chalk.blue('\n7. Preparing frontend...'));
|
|
const frontendDir = path.join(targetDir, 'frontend');
|
|
const frontendDistDir = path.join(frontendDir, 'dist');
|
|
if (fs.existsSync(frontendDir)) {
|
|
if (!fs.existsSync(frontendDistDir)) {
|
|
throw createConfigError('Prebuilt frontend output not found: `frontend/dist/`.', [
|
|
'The template should ship a prebuilt `frontend/dist/`.',
|
|
'Once dependencies finish installing, run `npm run build:frontend` in the project, then `pinme save`.',
|
|
]);
|
|
}
|
|
|
|
const patchResult = patchPrebuiltFrontendDist(frontendDistDir, workerData);
|
|
console.log(chalk.green(' Patched prebuilt frontend dist config'));
|
|
console.log(chalk.gray(
|
|
` Replacements: API URL ${patchResult.apiUrlReplacements}, auth ${patchResult.authReplacements}`,
|
|
));
|
|
|
|
// Upload to IPFS and capture the URL
|
|
console.log(chalk.blue(' Uploading to IPFS...'));
|
|
try {
|
|
const uploadResult = await uploadPath(frontendDistDir, {
|
|
action: 'project_create',
|
|
projectName: workerData.project_name,
|
|
uid: headers['token-address'],
|
|
});
|
|
printHighlightedUrl('Frontend URL', uploadResult.publicUrl, 'primary');
|
|
updateFrontendUrlInConfig(
|
|
path.join(targetDir, 'pinme.toml'),
|
|
uploadResult.publicUrl,
|
|
);
|
|
console.log(chalk.green(' Updated pinme.toml with frontend URL'));
|
|
} catch (error: any) {
|
|
console.log(chalk.yellow(' Warning: IPFS upload failed, you can upload manually later'));
|
|
}
|
|
}
|
|
|
|
console.log(chalk.green('\nProject created successfully.'));
|
|
console.log(chalk.gray(`\nProject Details:`));
|
|
console.log(chalk.gray(` API Domain: ${workerData.api_domain}`));
|
|
console.log(chalk.gray(` Project Name: ${workerData.project_name}`));
|
|
printHighlightedUrl(
|
|
'Project Management URL',
|
|
getProjectManagementUrl(workerData.project_name),
|
|
'management',
|
|
);
|
|
console.log(chalk.gray(`\nNext steps:`));
|
|
console.log(chalk.gray(` cd ${projectName}`));
|
|
console.log(chalk.gray(` pinme save`));
|
|
|
|
void tracker.trackEvent(
|
|
TRACK_EVENTS.projectCreateSuccess,
|
|
TRACK_PAGES.project,
|
|
{
|
|
a: resolveTrackAction(TRACK_EVENTS.projectCreateSuccess),
|
|
project_name: workerData.project_name,
|
|
template_branch: TEMPLATE_BRANCH,
|
|
force: Boolean(options.force),
|
|
},
|
|
);
|
|
|
|
process.exit(0);
|
|
} catch (error: any) {
|
|
void tracker.trackEvent(
|
|
TRACK_EVENTS.projectCreateFailed,
|
|
TRACK_PAGES.project,
|
|
{
|
|
a: resolveTrackAction(TRACK_EVENTS.projectCreateFailed),
|
|
project_name: options.name,
|
|
template_branch: TEMPLATE_BRANCH,
|
|
force: Boolean(options.force),
|
|
reason: getTrackErrorReason(error),
|
|
},
|
|
);
|
|
printCliError(error, 'Project creation failed.');
|
|
process.exit(1);
|
|
}
|
|
}
|