index.js 9.2 K raw
1
#!/usr/bin/env node
2
3
import fs from 'fs-extra';
4
import path from 'path';
5
import { fileURLToPath } from 'url';
6
import prompts from 'prompts';
7
import { program } from 'commander';
8
import chalk from 'chalk';
9
import ora from 'ora';
10
import { execa } from 'execa';
11
import degit from 'degit';
12
import figlet from 'figlet';
13
14
const __filename = fileURLToPath(import.meta.url);
15
const __dirname = path.dirname(__filename);
16
17
// GitHub repository for the template
18
const DEFAULT_REPO = 'stevedylandev/bhvr';
19
20
// Available templates
21
const TEMPLATES = {
22
  default: { branch: 'main', description: 'Basic setup with Bun, Hono, Vite and React' },
23
  tailwind: { branch: 'tailwindcss', description: 'Basic setup + TailwindCSS' },
24
  shadcn: { branch: 'shadcn-ui', description: 'Basic setup + TailwindCSS + shadcn/ui' }
25
};
26
27
// Function to display a fun banner
28
function displayBanner() {
29
  const text = figlet.textSync('bhvr', {
30
    font: 'Big',
31
    horizontalLayout: 'default',
32
    verticalLayout: 'default',
33
    width: 80,
34
    whitespaceBreak: true
35
  });
36
37
  console.log('\n');
38
  console.log(chalk.yellowBright(text));
39
  console.log(`\n${chalk.cyan('🦫 Lets build 🦫')}\n`);
40
  console.log(`${chalk.blue('https://github.com/stevedylandev/bhvr')}\n`);
41
}
42
43
// Set up the CLI program
44
program
45
  .name('create-bhvr')
46
  .description('Create a bhvr monorepo starter project')
47
  .argument('[project-directory]', 'directory to create the project in')
48
  .option('-y, --yes', 'skip confirmation prompts')
49
  .option('--ts, --typescript', 'use TypeScript (default)')
50
  .option('--repo <repo>', 'specify a custom GitHub repository as source', DEFAULT_REPO)
51
  .option('--template <template>', 'specify a template (default, tailwind, shadcn)', 'default')
52
  .option('--branch <branch>', 'specify a branch to use from the repository')
53
  .action(async (projectDirectory, options) => {
54
    try {
55
      displayBanner();
56
      const result = await createProject(projectDirectory, options);
57
      if (result) {
58
        console.log(chalk.green.bold('🎉 Project created successfully!'));
59
        console.log('\nNext steps:');
60
61
        if (!result.dependenciesInstalled) {
62
          console.log(chalk.cyan(`  cd ${result.projectName}`));
63
          console.log(chalk.cyan('  bun install'));
64
        } else {
65
          console.log(chalk.cyan(`  cd ${result.projectName}`));
66
        }
67
68
        console.log(chalk.cyan('  bun run dev:client   # Start the client'));
69
        console.log(chalk.cyan('  bun run dev:server   # Start the server in another terminal'));
70
        console.log(chalk.cyan('  bun run dev          # Start all'));
71
        process.exit(0);
72
      }
73
    } catch (err) {
74
      console.error(chalk.red('Error creating project:'), err);
75
      process.exit(1);
76
    }
77
  });
78
79
program.parse();
80
81
async function createProject(projectDirectory, options) {
82
  // If project directory not provided, prompt for it
83
  let projectName = projectDirectory;
84
85
  if (!projectName && !options.yes) {
86
    const response = await prompts({
87
      type: 'text',
88
      name: 'projectName',
89
      message: 'What is the name of your project?',
90
      initial: 'my-bhvr-app'
91
    });
92
93
    if (!response.projectName) {
94
      console.log(chalk.yellow('Project creation cancelled.'));
95
      return null;
96
    }
97
98
    projectName = response.projectName;
99
  } else if (!projectName) {
100
    projectName = 'my-bhvr-app';
101
  }
102
103
  // Template selection
104
  let templateChoice = options.template;
105
106
  if (!options.yes && !options.branch) {
107
    const templateChoices = Object.keys(TEMPLATES).map(key => ({
108
      title: `${key} (${TEMPLATES[key].description})`,
109
      value: key
110
    }));
111
112
    const templateResponse = await prompts({
113
      type: 'select',
114
      name: 'template',
115
      message: 'Select a template:',
116
      choices: templateChoices,
117
      initial: 0
118
    });
119
120
    if (templateResponse.template === undefined) {
121
      console.log(chalk.yellow('Project creation cancelled.'));
122
      return null;
123
    }
124
125
    templateChoice = templateResponse.template;
126
  }
127
128
  // Create the project directory
129
  const projectPath = path.resolve(process.cwd(), projectName);
130
131
  // Check if directory exists and is not empty
132
  if (fs.existsSync(projectPath)) {
133
    const files = fs.readdirSync(projectPath);
134
135
    if (files.length > 0 && !options.yes) {
136
      const { overwrite } = await prompts({
137
        type: 'confirm',
138
        name: 'overwrite',
139
        message: `The directory ${projectName} already exists and is not empty. Do you want to overwrite it?`,
140
        initial: false
141
      });
142
143
      if (!overwrite) {
144
        console.log(chalk.yellow('Project creation cancelled.'));
145
        return null;
146
      }
147
148
      // Clear directory if overwriting
149
      await fs.emptyDir(projectPath);
150
    }
151
  }
152
153
  // Create directory if it doesn't exist
154
  fs.ensureDirSync(projectPath);
155
156
  // Clone template from GitHub
157
  const repoPath = options.repo || DEFAULT_REPO;
158
  // Use provided branch, template branch, or default
159
  const branch = options.branch || (TEMPLATES[templateChoice] ? TEMPLATES[templateChoice].branch : 'main');
160
  const repoUrl = `${repoPath}#${branch}`;
161
162
  const spinner = ora('Downloading template...').start();
163
164
  try {
165
    const emitter = degit(repoUrl, {
166
      cache: false,
167
      force: true,
168
      verbose: false,
169
    });
170
171
    await emitter.clone(projectPath);
172
    spinner.succeed(`Template downloaded successfully (${templateChoice} template)`);
173
174
    // Update package.json with project name
175
    const pkgJsonPath = path.join(projectPath, 'package.json');
176
    if (fs.existsSync(pkgJsonPath)) {
177
      const pkgJson = await fs.readJson(pkgJsonPath);
178
      pkgJson.name = projectName;
179
      await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
180
    }
181
182
    // Remove the .git directory if it exists
183
    const gitDir = path.join(projectPath, '.git');
184
    if (fs.existsSync(gitDir)) {
185
      await fs.remove(gitDir);
186
      console.log(chalk.blue('Removed .git directory'));
187
    }
188
189
    // Initialize git repository?
190
    let gitInitialized = false;
191
192
    if (!options.yes) {
193
      const gitResponse = await prompts({
194
        type: 'confirm',
195
        name: 'initGit',
196
        message: 'Initialize a git repository?',
197
        initial: true
198
      });
199
200
      if (gitResponse.initGit) {
201
        try {
202
          spinner.start('Initializing git repository...');
203
          await execa('git', ['init'], { cwd: projectPath });
204
          await execa('git', ['add', '.'], { cwd: projectPath });
205
          await execa('git', ['commit', '-m', 'Initial commit from create-bhvr'], { cwd: projectPath });
206
          spinner.succeed('Git repository initialized');
207
          gitInitialized = true;
208
        } catch (err) {
209
          spinner.fail('Failed to initialize git repository. Is git installed?');
210
          console.error(chalk.red('Git error:'), err.message);
211
        }
212
      }
213
    } else {
214
      // If using --yes, automatically initialize git
215
      try {
216
        spinner.start('Initializing git repository...');
217
        await execa('git', ['init'], { cwd: projectPath });
218
        await execa('git', ['add', '.'], { cwd: projectPath });
219
        await execa('git', ['commit', '-m', 'Initial commit from create-bhvr'], { cwd: projectPath });
220
        spinner.succeed('Git repository initialized');
221
        gitInitialized = true;
222
      } catch (err) {
223
        spinner.fail('Failed to initialize git repository. Is git installed?');
224
      }
225
    }
226
227
    // Install dependencies?
228
    let dependenciesInstalled = false;
229
230
    if (!options.yes) {
231
      const depsResponse = await prompts({
232
        type: 'confirm',
233
        name: 'installDeps',
234
        message: 'Install dependencies?',
235
        initial: true
236
      });
237
238
      if (depsResponse.installDeps) {
239
        spinner.start('Installing dependencies...');
240
        try {
241
          // Try with bun first
242
          await execa('bun', ['install'], { cwd: projectPath });
243
          spinner.succeed('Dependencies installed with bun');
244
          dependenciesInstalled = true;
245
        } catch (bunErr) {
246
          // If bun fails, try with npm
247
          try {
248
            spinner.text = 'Installing dependencies with npm...';
249
            await execa('npm', ['install'], { cwd: projectPath });
250
            spinner.succeed('Dependencies installed with npm');
251
            dependenciesInstalled = true;
252
          } catch (npmErr) {
253
            spinner.fail('Failed to install dependencies.');
254
            console.log(chalk.yellow('You can install them manually after navigating to the project directory.'));
255
          }
256
        }
257
      }
258
    } else {
259
      // If using --yes, automatically install dependencies
260
      spinner.start('Installing dependencies...');
261
      try {
262
        await execa('bun', ['install'], { cwd: projectPath });
263
        spinner.succeed('Dependencies installed with bun');
264
        dependenciesInstalled = true;
265
      } catch (bunErr) {
266
        try {
267
          spinner.text = 'Installing dependencies with npm...';
268
          await execa('npm', ['install'], { cwd: projectPath });
269
          spinner.succeed('Dependencies installed with npm');
270
          dependenciesInstalled = true;
271
        } catch (npmErr) {
272
          spinner.fail('Failed to install dependencies. You can install them manually later.');
273
        }
274
      }
275
    }
276
277
    return {
278
      projectName,
279
      gitInitialized,
280
      dependenciesInstalled,
281
      template: templateChoice,
282
    };
283
  } catch (err) {
284
    spinner.fail('Failed to download template');
285
    throw err;
286
  }
287
}