create-sumit-app 1.0.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,27 +1,24 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from 'module';
3
- import { fileURLToPath } from 'url';
4
- import { Command } from 'commander';
5
- import { execa } from 'execa';
6
- import prompts from 'prompts';
7
- import chalk from 'chalk';
8
- import fs from 'fs-extra';
9
- import degit from 'degit';
10
- import path from 'path';
11
- import ora from 'ora';
12
- import { TEMPLATES, getTemplate, listTemplates } from './lib/templates.js';
13
- import { Logger } from './lib/logger.js';
14
- import { loadConfig, updateConfig, resetConfig, showConfig, getConfigPath, } from './lib/config.js';
15
- import { getPackageManagerInfo, checkForUpdates, isDirectoryEmpty, validateProjectName, formatDuration,
16
- // initializeGitRepository,
17
- } from './lib/utils.js';
2
+ import { createRequire } from "module";
3
+ import { fileURLToPath } from "url";
4
+ import { Command } from "commander";
5
+ import { execa } from "execa";
6
+ import prompts from "prompts";
7
+ import chalk from "chalk";
8
+ import fs from "fs-extra";
9
+ import degit from "degit";
10
+ import path from "path";
11
+ import ora from "ora";
12
+ import { loadConfig, updateConfig, resetConfig, showConfig, getConfigPath } from "./lib/config.js";
13
+ import { PROJECTS, PRESETS, BASE_TEMPLATE } from "./lib/templates.js";
14
+ import { Logger } from "./lib/logger.js";
15
+ import { getPackageManagerInfo, checkForUpdates, isDirectoryEmpty, validateProjectName, formatDuration,
16
+ // enableWindowsLongPaths,
17
+ getPreset, listPresets, listProjects, getTemplate, listTemplates, } from "./lib/utils.js";
18
18
  const __filename = fileURLToPath(import.meta.url);
19
19
  const __dirname = path.dirname(__filename);
20
- function getTemplatePath(template) {
21
- return template.path || `templates/${template.name}`;
22
- }
23
20
  async function cleanupGitDirectory(projectPath, logger) {
24
- const gitDir = path.join(projectPath, '.git');
21
+ const gitDir = path.join(projectPath, ".git");
25
22
  if (!(await fs.pathExists(gitDir))) {
26
23
  return;
27
24
  }
@@ -29,17 +26,15 @@ async function cleanupGitDirectory(projectPath, logger) {
29
26
  while (retries > 0) {
30
27
  try {
31
28
  await fs.remove(gitDir);
32
- logger.verbose('Removed template .git directory');
29
+ logger.verbose("Removed template .git directory");
33
30
  return;
34
31
  }
35
32
  catch (error) {
36
33
  retries--;
37
- if (error.code === 'EBUSY' || error.code === 'EPERM') {
34
+ if (error.code === "EBUSY" || error.code === "EPERM") {
38
35
  if (retries > 0) {
39
36
  logger.verbose(`Git cleanup failed, retrying... (${retries} attempts left)`);
40
- // Wait a bit before retrying
41
37
  await new Promise((resolve) => setTimeout(resolve, 1000));
42
- // Try to make files writable on Windows
43
38
  try {
44
39
  await makeGitFilesWritable(gitDir);
45
40
  }
@@ -48,20 +43,18 @@ async function cleanupGitDirectory(projectPath, logger) {
48
43
  }
49
44
  }
50
45
  else {
51
- logger.warn('Could not remove .git directory automatically');
52
- logger.info('You may need to manually delete the .git folder if it exists');
46
+ logger.warn("Could not remove .git directory automatically");
47
+ logger.info("You may need to manually delete the .git folder if it exists");
53
48
  return;
54
49
  }
55
50
  }
56
51
  else {
57
- // For other errors, just warn and continue
58
52
  logger.verbose(`Git cleanup error: ${error.message}`);
59
53
  return;
60
54
  }
61
55
  }
62
56
  }
63
57
  }
64
- // Helper function to make git files writable
65
58
  async function makeGitFilesWritable(gitDir) {
66
59
  try {
67
60
  const files = await fs.readdir(gitDir, { withFileTypes: true });
@@ -72,7 +65,7 @@ async function makeGitFilesWritable(gitDir) {
72
65
  }
73
66
  else {
74
67
  try {
75
- await fs.chmod(fullPath, 0o666); // Make file writable
68
+ await fs.chmod(fullPath, 0o666);
76
69
  }
77
70
  catch (error) {
78
71
  // Ignore individual file errors
@@ -85,48 +78,44 @@ async function makeGitFilesWritable(gitDir) {
85
78
  }
86
79
  }
87
80
  async function selectPackageManager(config, logger, packageManagerName) {
88
- // Available package managers with descriptions
89
81
  const availableManagers = [
90
82
  {
91
- name: 'bun',
92
- description: '⚡ Ultra-fast JavaScript runtime and package manager',
83
+ name: "bun",
84
+ description: "⚡ Ultra-fast JavaScript runtime and package manager",
85
+ },
86
+ {
87
+ name: "pnpm",
88
+ description: "📦 Fast, disk space efficient package manager",
89
+ },
90
+ {
91
+ name: "yarn",
92
+ description: "🐈 Fast, reliable, and secure dependency management",
93
+ },
94
+ {
95
+ name: "npm",
96
+ description: "📦 Default Node.js package manager",
93
97
  },
94
- // {
95
- // name: 'pnpm',
96
- // description: '📦 Fast, disk space efficient package manager',
97
- // },
98
- // {
99
- // name: 'yarn',
100
- // description: '🐈 Fast, reliable, and secure dependency management',
101
- // },
102
- // {
103
- // name: 'npm',
104
- // description: '📦 Simple and widely used Node.js package manager',
105
- // },
106
98
  ];
107
- // If package manager specified via CLI, use it
108
99
  if (packageManagerName) {
109
100
  const manager = availableManagers.find((m) => m.name === packageManagerName);
110
101
  if (!manager) {
111
102
  logger.error(`Package manager "${packageManagerName}" not found.`);
112
- logger.info('Available package managers:');
103
+ logger.info("Available package managers:");
113
104
  availableManagers.forEach((m) => logger.info(` • ${m.name} - ${m.description}`));
114
105
  process.exit(1);
115
106
  }
116
107
  logger.verbose(`Using CLI specified package manager: ${manager.name}`);
117
108
  return manager.name;
118
109
  }
119
- // If default package manager in config, use it
120
110
  if (config.packageManager) {
121
111
  logger.verbose(`Using config default package manager: ${config.packageManager}`);
122
112
  return config.packageManager;
123
113
  }
124
- // Check for lockfiles in the CURRENT directory for auto-detection
125
114
  const lockFiles = [
126
- { name: 'bun', file: 'bun.lockb' },
127
- { name: 'pnpm', file: 'pnpm-lock.yaml' },
128
- { name: 'yarn', file: 'yarn.lock' },
129
- { name: 'npm', file: 'package-lock.json' },
115
+ { name: "bun", file: "bun.lockb" },
116
+ { name: "pnpm", file: "pnpm-lock.yaml" },
117
+ { name: "yarn", file: "yarn.lock" },
118
+ { name: "npm", file: "package-lock.json" },
130
119
  ];
131
120
  for (const lockFile of lockFiles) {
132
121
  if (await fs.pathExists(path.join(process.cwd(), lockFile.file))) {
@@ -134,65 +123,187 @@ async function selectPackageManager(config, logger, packageManagerName) {
134
123
  return lockFile.name;
135
124
  }
136
125
  }
137
- // Interactive selection (no auto-detection from global availability)
138
126
  logger.newLine();
139
- logger.log('📦 Choose a package manager:');
127
+ logger.log("📦 Choose a package manager:");
140
128
  const { selectedManager } = await prompts({
141
- type: 'select',
142
- name: 'selectedManager',
143
- message: 'Select a package manager',
129
+ type: "select",
130
+ name: "selectedManager",
131
+ message: "Select a package manager",
144
132
  choices: availableManagers.map((manager) => ({
145
133
  title: `${manager.name}`,
146
134
  description: manager.description,
147
135
  value: manager.name,
148
136
  })),
149
- initial: 0, // Default to bun (first option)
137
+ initial: 0,
150
138
  });
151
139
  if (!selectedManager) {
152
- logger.error('Package manager selection cancelled');
140
+ logger.error("Package manager selection cancelled");
153
141
  process.exit(0);
154
142
  }
155
143
  return selectedManager;
156
144
  }
157
- async function selectTemplate(config, logger, templateName) {
158
- // If template specified via CLI, use it
159
- if (templateName) {
160
- const template = getTemplate(templateName);
161
- if (!template) {
162
- logger.error(`Template "${templateName}" not found.`);
163
- logger.info('Available templates:');
164
- TEMPLATES.forEach((t) => logger.info(` ${t.name} - ${t.description}`));
145
+ async function selectPresetOrProjects(config, logger, presetName, projectNames) {
146
+ // If specific projects provided via CLI, use them
147
+ if (projectNames && projectNames.length > 0) {
148
+ const validProjects = projectNames.filter((p) => PROJECTS[p]);
149
+ if (validProjects.length !== projectNames.length) {
150
+ const invalid = projectNames.filter((p) => !PROJECTS[p]);
151
+ logger.error(`Invalid project(s): ${invalid.join(", ")}`);
152
+ logger.info(`Available projects: ${Object.keys(PROJECTS).join(", ")}`);
165
153
  process.exit(1);
166
154
  }
167
- return template;
155
+ return validProjects;
168
156
  }
169
- // If default template in config, use it
170
- if (config.defaultTemplate) {
171
- const template = getTemplate(config.defaultTemplate);
172
- if (template) {
173
- logger.verbose(`Using default template: ${template.name}`);
174
- return template;
157
+ // If preset specified via CLI, use it
158
+ if (presetName) {
159
+ const preset = getPreset(presetName);
160
+ if (!preset) {
161
+ logger.error(`Preset "${presetName}" not found.`);
162
+ logger.info("Available presets:");
163
+ PRESETS.forEach((p) => logger.info(` • ${p.name} - ${p.description}`));
164
+ process.exit(1);
165
+ }
166
+ if (preset.name === "custom") {
167
+ return await selectProjects(logger);
175
168
  }
169
+ return preset.projects;
176
170
  }
177
- // Interactive selection
178
- // logger.newLine();
179
- logger.log('🎨 Choose a template for your project:');
180
- const { selectedTemplate } = await prompts({
181
- type: 'select',
182
- name: 'selectedTemplate',
183
- message: 'Select a template',
184
- choices: TEMPLATES.map((template, index) => ({
185
- title: `${template.name}`,
186
- description: template.description,
187
- value: template,
171
+ // If default preset in config, use it
172
+ if (config.defaultPreset) {
173
+ const preset = getPreset(config.defaultPreset);
174
+ if (preset && preset.name !== "custom") {
175
+ logger.verbose(`Using default preset: ${preset.name}`);
176
+ return preset.projects;
177
+ }
178
+ }
179
+ // Interactive project selection - show multi-select checkboxes directly
180
+ return await selectProjects(logger);
181
+ }
182
+ async function selectProjects(logger) {
183
+ const { selectedProjects } = await prompts({
184
+ type: "multiselect",
185
+ name: "selectedProjects",
186
+ message: "Choose projects",
187
+ choices: Object.values(PROJECTS).map((project) => ({
188
+ title: project.name,
189
+ description: project.description,
190
+ value: project.name,
191
+ selected: true, // All selected by default
188
192
  })),
189
- initial: 0,
193
+ min: 1,
194
+ hint: "- Space to toggle, Enter to confirm",
195
+ instructions: false,
190
196
  });
191
- if (!selectedTemplate) {
192
- logger.error('Template selection cancelled');
197
+ if (!selectedProjects || selectedProjects.length === 0) {
198
+ logger.error("At least one project must be selected");
193
199
  process.exit(0);
194
200
  }
195
- return selectedTemplate;
201
+ return selectedProjects;
202
+ }
203
+ function generatePackageJson(projectName, selectedProjects, packageManager) {
204
+ const hasMobile = selectedProjects.includes("mobile");
205
+ // Package manager versions
206
+ const packageManagerVersions = {
207
+ bun: "bun@1.3.6",
208
+ pnpm: "pnpm@10.28.1",
209
+ yarn: "yarn@4.12.0",
210
+ npm: "npm@11.8.0",
211
+ };
212
+ // Clean commands per package manager
213
+ const cleanCommands = {
214
+ bun: "turbo run clean && node scripts/clean.js && bun pm cache rm",
215
+ pnpm: "turbo run clean && node scripts/clean.js && pnpm store prune",
216
+ yarn: "turbo run clean && node scripts/clean.js && yarn cache clean",
217
+ npm: "turbo run clean && node scripts/clean.js && npm cache clean --force",
218
+ };
219
+ const packageJson = {
220
+ name: projectName,
221
+ version: "1.0.0",
222
+ private: true,
223
+ strict: true,
224
+ scripts: {
225
+ dev: "turbo run dev",
226
+ build: "turbo run build",
227
+ lint: "turbo run lint",
228
+ format: "prettier --write .",
229
+ "check-types": "turbo run check-types",
230
+ clean: cleanCommands[packageManager] || cleanCommands.bun,
231
+ },
232
+ devDependencies: {
233
+ "@packages/eslint-config": packageManager === "npm" ? "*" : "workspace:^",
234
+ "@packages/typescript-config": packageManager === "npm" ? "*" : "workspace:^",
235
+ "@types/node": "^25.0.10",
236
+ prettier: "^3.8.1",
237
+ "prettier-plugin-tailwindcss": "^0.7.2",
238
+ rimraf: "^6.1.2",
239
+ turbo: "^2.7.6",
240
+ typescript: "^5.9.3",
241
+ },
242
+ engines: {
243
+ node: ">=22",
244
+ },
245
+ workspaces: ["projects/*", "packages/*"],
246
+ packageManager: packageManagerVersions[packageManager] || packageManagerVersions.bun,
247
+ };
248
+ // Add expo-dev-menu resolution only if mobile project is selected
249
+ if (hasMobile) {
250
+ packageJson.resolutions = {
251
+ "expo-dev-menu": "^7.0.10",
252
+ };
253
+ }
254
+ return packageJson;
255
+ }
256
+ async function generatePackageManagerConfig(projectPath, packageManager, logger) {
257
+ switch (packageManager) {
258
+ case "yarn":
259
+ // Yarn: Use hoisted node_modules for compatibility
260
+ const yarnrcContent = `# Yarn configuration - hoisted node_modules for compatibility
261
+ nodeLinker: node-modules
262
+ `;
263
+ await fs.writeFile(path.join(projectPath, ".yarnrc.yml"), yarnrcContent);
264
+ logger.verbose("Created .yarnrc.yml with node-modules linker");
265
+ break;
266
+ case "pnpm":
267
+ // pnpm: Use hoisted node_modules for compatibility
268
+ const npmrcContent = `# pnpm configuration - hoisted node_modules for compatibility
269
+ node-linker=hoisted
270
+ shamefully-hoist=true
271
+ `;
272
+ await fs.writeFile(path.join(projectPath, ".npmrc"), npmrcContent);
273
+ // pnpm requires pnpm-workspace.yaml instead of workspaces in package.json
274
+ const pnpmWorkspaceContent = `# pnpm workspace configuration
275
+ packages:
276
+ - "projects/*"
277
+ - "packages/*"
278
+ `;
279
+ await fs.writeFile(path.join(projectPath, "pnpm-workspace.yaml"), pnpmWorkspaceContent);
280
+ logger.verbose("Created .npmrc and pnpm-workspace.yaml");
281
+ // Verify workspace structure
282
+ const packagesPath = path.join(projectPath, "packages");
283
+ if (await fs.pathExists(packagesPath)) {
284
+ const pkgs = await fs.readdir(packagesPath);
285
+ logger.verbose(`Found packages: ${pkgs.join(", ")}`);
286
+ }
287
+ else {
288
+ logger.warn("Warning: packages/ directory not found in project root");
289
+ }
290
+ break;
291
+ case "bun":
292
+ // Bun: Use hoisted node_modules for compatibility
293
+ const bunfigContent = `# Bun configuration - hoisted node_modules for compatibility
294
+ [install]
295
+ linker = "hoisted"
296
+ `;
297
+ await fs.writeFile(path.join(projectPath, "bunfig.toml"), bunfigContent);
298
+ logger.verbose("Created bunfig.toml with symlink backend");
299
+ break;
300
+ case "npm":
301
+ // npm: Uses node_modules by default, no special config needed
302
+ logger.verbose("npm uses node_modules by default, no config needed");
303
+ break;
304
+ default:
305
+ break;
306
+ }
196
307
  }
197
308
  async function createProject(projectName, options = {}) {
198
309
  const startTime = Date.now();
@@ -200,37 +311,46 @@ async function createProject(projectName, options = {}) {
200
311
  const config = await loadConfig();
201
312
  // Show banner
202
313
  logger.banner();
314
+ // Enable Windows Long Paths if needed
315
+ // await enableWindowsLongPaths(logger);
203
316
  // Check for updates
204
317
  if (!config.skipUpdateCheck && !options.verbose) {
205
318
  await checkForUpdates(logger);
206
319
  }
320
+ // Handle legacy --template option
321
+ let presetName = options.preset;
322
+ if (options.template && !presetName) {
323
+ const template = getTemplate(options.template);
324
+ if (template) {
325
+ presetName = template.name;
326
+ logger.warn(`--template is deprecated, use --preset instead`);
327
+ }
328
+ }
207
329
  let targetDir = projectName;
208
330
  // Handle project name
209
331
  if (!targetDir) {
210
332
  const { projectName: inputName } = await prompts({
211
- type: 'text',
212
- name: 'projectName',
213
- message: 'What is your project named?',
214
- initial: 'my-sumit-app',
333
+ type: "text",
334
+ name: "projectName",
335
+ message: "What is your project named?",
336
+ initial: "my-sumit-app",
215
337
  validate: async (value) => {
216
338
  if (!value.trim())
217
- return 'Project name is required';
218
- // Allow dot (.) for current directory
219
- if (value === '.')
339
+ return "Project name is required";
340
+ if (value === ".")
220
341
  return true;
221
342
  const validation = await validateProjectName(value);
222
343
  return validation.valid || validation.message;
223
344
  },
224
345
  });
225
346
  if (!inputName) {
226
- logger.warn('Project creation cancelled.');
347
+ logger.warn("Project creation cancelled.");
227
348
  process.exit(0);
228
349
  }
229
350
  targetDir = inputName;
230
351
  }
231
352
  else {
232
- // Validate provided project name (allow dot for current directory)
233
- if (targetDir !== '.') {
353
+ if (targetDir !== ".") {
234
354
  const validation = await validateProjectName(targetDir);
235
355
  if (!validation.valid) {
236
356
  logger.error(validation.message);
@@ -238,253 +358,331 @@ async function createProject(projectName, options = {}) {
238
358
  }
239
359
  }
240
360
  }
241
- // Type guard to ensure targetDir is defined
242
361
  if (!targetDir) {
243
- logger.error('Project name is required');
362
+ logger.error("Project name is required");
244
363
  process.exit(1);
245
364
  }
246
365
  // Handle current directory installation
247
- let resolvedProjectPath; // ← Changed name to avoid conflict
248
- let displayPath; // ← Changed name to avoid conflict
249
- if (targetDir === '.') {
250
- // Install in current directory
366
+ let resolvedProjectPath;
367
+ let displayPath;
368
+ if (targetDir === ".") {
251
369
  resolvedProjectPath = process.cwd();
252
- displayPath = 'current directory';
253
- // Validate current directory name for package.json compatibility
370
+ displayPath = "current directory";
254
371
  const currentDirName = path.basename(resolvedProjectPath);
255
372
  const validation = await validateProjectName(currentDirName);
256
373
  if (!validation.valid) {
257
374
  logger.error(`Current directory name "${currentDirName}" is not a valid package name.`);
258
375
  logger.error(validation.message);
259
- logger.info('Please rename your directory to use lowercase letters, numbers, hyphens, underscores, and dots only.');
376
+ logger.info("Please rename your directory to use lowercase letters, numbers, hyphens, underscores, and dots only.");
260
377
  process.exit(1);
261
378
  }
262
379
  }
263
380
  else {
264
- // Install in new directory
265
381
  resolvedProjectPath = path.resolve(process.cwd(), targetDir);
266
382
  const relativeProjectPath = path.relative(process.cwd(), resolvedProjectPath);
267
- displayPath = relativeProjectPath || 'current directory';
383
+ displayPath = relativeProjectPath || "current directory";
268
384
  }
269
385
  logger.newLine();
270
- logger.step(1, 4, `Creating project in ${chalk.cyan(displayPath)}`);
386
+ logger.step(1, 5, `Creating project in ${chalk.cyan(displayPath)}`);
271
387
  // Check if directory exists and is not empty
272
388
  if (await fs.pathExists(resolvedProjectPath)) {
273
389
  if (!(await isDirectoryEmpty(resolvedProjectPath))) {
274
- logger.error(`Directory "${targetDir === '.' ? 'current directory' : targetDir}" already exists and is not empty.`);
390
+ logger.error(`Directory "${targetDir === "." ? "current directory" : targetDir}" already exists and is not empty.`);
275
391
  const { overwrite } = await prompts({
276
- type: 'confirm',
277
- name: 'overwrite',
278
- message: 'Remove existing files and continue?',
392
+ type: "confirm",
393
+ name: "overwrite",
394
+ message: "Remove existing files and continue?",
279
395
  initial: false,
280
396
  });
281
397
  if (overwrite) {
282
- // Add spinner while deleting files
283
398
  const deleteSpinner = ora({
284
- text: 'Removing existing files...',
285
- spinner: 'dots',
399
+ text: "Removing existing files...",
400
+ spinner: "dots",
286
401
  }).start();
287
402
  try {
288
403
  await fs.emptyDir(resolvedProjectPath);
289
- deleteSpinner.succeed(chalk.green('Existing files removed'));
404
+ deleteSpinner.succeed(chalk.green("Existing files removed"));
290
405
  }
291
406
  catch (error) {
292
- deleteSpinner.fail(chalk.red('Failed to remove existing files'));
407
+ deleteSpinner.fail(chalk.red("Failed to remove existing files"));
293
408
  logger.error(`Directory cleanup failed: ${error}`);
294
409
  process.exit(1);
295
410
  }
296
411
  }
297
412
  else {
298
- logger.warn('Project creation cancelled.');
413
+ logger.warn("Project creation cancelled.");
299
414
  process.exit(0);
300
415
  }
301
416
  }
302
417
  }
303
- // Select template
418
+ // Select preset or custom projects
304
419
  logger.newLine();
305
- const template = await selectTemplate(config, logger, options.template);
420
+ const selectedProjects = await selectPresetOrProjects(config, logger, presetName, options.projects);
306
421
  logger.newLine();
307
- logger.step(2, 4, `Using template: ${chalk.cyan(template.name)}`);
308
- // Show template features
309
- if (options.verbose) {
310
- logger.box(template.features.map((f) => `• ${f}`).join('\n'), `✨ ${template.name} template's features`);
311
- }
312
- // Clone template with sparse checkout for subdirectories
313
- const downloadSpinner = ora({
314
- text: 'Downloading template...',
315
- spinner: 'dots12',
422
+ logger.step(2, 5, `Selected projects: ${chalk.cyan(selectedProjects.join(", "))}`);
423
+ // Download base template
424
+ const baseSpinner = ora({
425
+ text: "Downloading base template...",
426
+ spinner: "dots12",
316
427
  }).start();
317
428
  try {
318
- logger.verbose(`Downloading from: ${template.url}`);
319
- // Ensure target directory is ready
320
- if (targetDir !== '.' && (await fs.pathExists(resolvedProjectPath))) {
321
- await fs.remove(resolvedProjectPath);
322
- }
323
- // Ensure directory exists
324
429
  await fs.ensureDir(resolvedProjectPath);
325
- // For templates with subdirectories, use degit
326
- if (template.url.includes('SumitApp.git')) {
327
- const templatePath = getTemplatePath(template);
328
- // Extract repo info from URL
329
- // https://github.com/sumittttpaul/SumitApp.git -> sumittttpaul/SumitApp
330
- const repoMatch = template.url.match(/github\.com[/:]([\w-]+)\/([\w-]+)/);
430
+ const repoMatch = BASE_TEMPLATE.url.match(/github\.com[\/:]([\w-]+)\/([\w-]+)/);
431
+ if (!repoMatch) {
432
+ throw new Error("Invalid GitHub repository URL");
433
+ }
434
+ const [, owner, repo] = repoMatch;
435
+ const repoPath = `${owner}/${repo.replace(".git", "")}/${BASE_TEMPLATE.path}`;
436
+ const emitter = degit(repoPath, {
437
+ cache: false,
438
+ force: true,
439
+ verbose: options.verbose,
440
+ });
441
+ await emitter.clone(resolvedProjectPath);
442
+ baseSpinner.succeed(chalk.green("Base template downloaded!"));
443
+ }
444
+ catch (error) {
445
+ baseSpinner.fail(chalk.red("Failed to download base template"));
446
+ logger.error(`Download failed: ${error.message || error}`);
447
+ process.exit(1);
448
+ }
449
+ // Download selected projects
450
+ logger.newLine();
451
+ logger.step(3, 5, "Downloading selected projects...");
452
+ const projectsDir = path.join(resolvedProjectPath, "projects");
453
+ await fs.ensureDir(projectsDir);
454
+ for (const projectName of selectedProjects) {
455
+ const project = PROJECTS[projectName];
456
+ const projectSpinner = ora({
457
+ text: `Downloading ${projectName}...`,
458
+ spinner: "dots",
459
+ }).start();
460
+ try {
461
+ const repoMatch = BASE_TEMPLATE.url.match(/github\.com[\/:]([\w-]+)\/([\w-]+)/);
331
462
  if (!repoMatch) {
332
- throw new Error('Invalid GitHub repository URL');
463
+ throw new Error("Invalid GitHub repository URL");
333
464
  }
334
465
  const [, owner, repo] = repoMatch;
335
- const repoPath = `${owner}/${repo.replace('.git', '')}/${templatePath}`;
336
- // degit downloads ONLY this specific folder
466
+ const repoPath = `${owner}/${repo.replace(".git", "")}/${project.path}`;
337
467
  const emitter = degit(repoPath, {
338
468
  cache: false,
339
469
  force: true,
340
470
  verbose: options.verbose,
341
471
  });
342
- // Download directly to target directory
343
- await emitter.clone(resolvedProjectPath);
344
- logger.verbose(`Downloaded template from: ${repoPath}`);
472
+ const projectTargetPath = path.join(projectsDir, project.name);
473
+ await emitter.clone(projectTargetPath);
474
+ projectSpinner.succeed(chalk.green(`Downloaded ${projectName}`));
345
475
  }
346
- else {
347
- // For standalone repos, still use degit
348
- const repoMatch = template.url.match(/github\.com[/:]([\w-]+)\/([\w-]+)/);
349
- if (repoMatch) {
350
- const [, owner, repo] = repoMatch;
351
- const emitter = degit(`${owner}/${repo.replace('.git', '')}`, {
352
- cache: false,
353
- force: true,
354
- verbose: options.verbose,
355
- });
356
- await emitter.clone(resolvedProjectPath);
357
- }
358
- else {
359
- throw new Error('Invalid repository URL');
360
- }
476
+ catch (error) {
477
+ projectSpinner.fail(chalk.red(`Failed to download ${projectName}`));
478
+ logger.error(`Download failed: ${error.message || error}`);
479
+ process.exit(1);
361
480
  }
362
- downloadSpinner.succeed(chalk.green('Template downloaded successfully!'));
363
481
  }
364
- catch (error) {
365
- downloadSpinner.fail(chalk.red('Failed to download template'));
366
- logger.error(`Download failed: ${error.message || error}`);
367
- logger.info('Make sure the repository and template folder exist and are accessible.');
368
- // Clean up failed download
369
- try {
370
- if (await fs.pathExists(resolvedProjectPath)) {
371
- await fs.remove(resolvedProjectPath);
482
+ // Detect/Select package manager (moved before package.json generation)
483
+ const packageManager = await selectPackageManager(config, logger, options.packageManager);
484
+ const pmInfo = getPackageManagerInfo(packageManager);
485
+ // npm Fix: Recursively replace "workspace:" protocol with "*" in all package.json files
486
+ // because npm doesn't support "workspace:" protocol (it uses "*" or versions directly)
487
+ if (packageManager === "npm") {
488
+ logger.verbose("Adjusting workspace dependencies for npm compatibility...");
489
+ const scanDir = async (dir) => {
490
+ const entries = await fs.readdir(dir, { withFileTypes: true });
491
+ for (const entry of entries) {
492
+ const fullPath = path.join(dir, entry.name);
493
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
494
+ await scanDir(fullPath);
495
+ }
496
+ else if (entry.name === "package.json") {
497
+ try {
498
+ const content = await fs.readFile(fullPath, "utf-8");
499
+ if (content.includes("workspace:")) {
500
+ logger.verbose(`Fixing workspace protocol in ${path.relative(resolvedProjectPath, fullPath)}`);
501
+ // Replace "workspace:^" or "workspace:*" with "*" for npm
502
+ const fixedContent = content.replace(/"workspace:\^"/g, '"*"').replace(/"workspace:\*"/g, '"*"');
503
+ await fs.writeFile(fullPath, fixedContent);
504
+ }
505
+ }
506
+ catch (e) {
507
+ // Ignore read errors
508
+ }
509
+ }
372
510
  }
373
- }
374
- catch (cleanupError) {
375
- // Ignore
376
- }
377
- process.exit(1);
511
+ };
512
+ await scanDir(resolvedProjectPath);
378
513
  }
514
+ // Generate package.json based on selected projects and package manager
515
+ const actualProjectName = targetDir === "." ? path.basename(resolvedProjectPath) : targetDir;
516
+ const packageJsonContent = generatePackageJson(actualProjectName, selectedProjects, packageManager);
517
+ await fs.writeJson(path.join(resolvedProjectPath, "package.json"), packageJsonContent, { spaces: 2 });
518
+ logger.verbose("Generated package.json with correct configuration");
519
+ // Generate package manager config files to ensure node_modules in root
520
+ await generatePackageManagerConfig(resolvedProjectPath, packageManager, logger);
379
521
  // Clean up git directory
380
522
  await cleanupGitDirectory(resolvedProjectPath, logger);
381
- logger.verbose('Removed template .git directory');
382
- // Detect/Select package manager
383
- const packageManager = await selectPackageManager(config, logger, options.packageManager);
384
- const pmInfo = getPackageManagerInfo(packageManager);
385
523
  logger.newLine();
386
- logger.step(3, 4, `Using package manager: ${chalk.cyan(packageManager)}`);
524
+ logger.step(4, 5, `Using package manager: ${chalk.cyan(packageManager)}`);
387
525
  // Install dependencies
388
526
  if (!options.skipInstall) {
389
527
  const installSpinner = ora({
390
528
  text: `Installing dependencies with ${packageManager}...`,
391
- spinner: 'bouncingBar',
529
+ spinner: "bouncingBar",
392
530
  }).start();
393
531
  try {
394
532
  await execa(pmInfo.command, pmInfo.installArgs, {
395
533
  cwd: resolvedProjectPath,
396
- stdio: options.verbose ? 'inherit' : 'pipe',
534
+ stdio: options.verbose ? "inherit" : "pipe",
397
535
  });
398
- installSpinner.succeed(chalk.green('Dependencies installed successfully!'));
536
+ installSpinner.succeed(chalk.green("Dependencies installed successfully!"));
399
537
  }
400
538
  catch (error) {
401
- installSpinner.fail(chalk.red('Failed to install dependencies'));
539
+ installSpinner.fail(chalk.red("Failed to install dependencies"));
402
540
  logger.error(`Dependency installation failed: ${error}`);
403
- logger.warn('You can install dependencies manually by running:');
404
- logger.info(` cd ${targetDir}`);
405
- logger.info(` ${pmInfo.command} ${pmInfo.installArgs.join(' ')}`);
541
+ logger.warn("You can install dependencies manually by running:");
542
+ logger.info(`cd ${targetDir}`);
543
+ logger.info(`${pmInfo.command} ${pmInfo.installArgs.join(" ")}`);
406
544
  }
407
545
  }
408
546
  else {
409
- logger.info('Skipped dependency installation');
410
- }
411
- // Initialize git repository
412
- // logger.step(4, 5, 'Initializing git repository...');
413
- // logger.newLine();
414
- // if (options.git !== false && config.git !== false) {
415
- // const gitSuccess = await initializeGitRepository(projectPath, logger);
416
- // if (gitSuccess) {
417
- // logger.success('Git repository initialized');
418
- // } else {
419
- // logger.warn(
420
- // 'Git initialization failed, but project was created successfully'
421
- // );
422
- // }
423
- // } else {
424
- // logger.info('Skipped git initialization');
425
- // }
426
- // Get the actual project name for display
427
- const actualProjectName = targetDir === '.' ? path.basename(resolvedProjectPath) : targetDir;
547
+ logger.info("Skipped dependency installation");
548
+ }
428
549
  // Success message
429
550
  const duration = formatDuration(Date.now() - startTime);
430
551
  logger.newLine();
431
- logger.step(4, 4, 'Project setup complete!');
552
+ logger.step(5, 5, "Project setup complete!");
432
553
  logger.newLine();
433
554
  logger.success(`🎉 Successfully created ${chalk.green(actualProjectName)} in ${chalk.green(duration)}`);
434
555
  logger.newLine();
435
- // Next steps - show actual commands based on directory
556
+ // Next steps
436
557
  const nextSteps = [];
437
- if (targetDir === '.') {
438
- // Current directory - no cd command needed
439
- nextSteps.push(`${pmInfo.command} ${pmInfo.name === 'npm' ? 'run ' : ''}dev`);
558
+ if (targetDir === ".") {
559
+ nextSteps.push(`${pmInfo.command} ${pmInfo.name === "npm" ? "run " : ""}dev`);
440
560
  }
441
561
  else {
442
- // New directory - include cd command
443
- nextSteps.push(`cd ${chalk.hex('#FFFFFF')(actualProjectName)}`);
444
- nextSteps.push(`${pmInfo.command} ${pmInfo.name === 'npm' ? 'run ' : ''}dev`);
562
+ nextSteps.push(`cd ${chalk.hex("#FFFFFF")(actualProjectName)}`);
563
+ nextSteps.push(`${pmInfo.command} ${pmInfo.name === "npm" ? "run " : ""}dev`);
445
564
  }
446
565
  if (options.skipInstall) {
447
- // Insert install command at appropriate position
448
- const installCmd = `${pmInfo.command} ${pmInfo.installArgs.join(' ')}`;
449
- if (targetDir === '.') {
450
- nextSteps.splice(0, 0, installCmd); // Add as first step
566
+ const installCmd = `${pmInfo.command} ${pmInfo.installArgs.join(" ")}`;
567
+ if (targetDir === ".") {
568
+ nextSteps.splice(0, 0, installCmd);
451
569
  }
452
570
  else {
453
- nextSteps.splice(1, 0, installCmd); // Add after cd command
571
+ nextSteps.splice(1, 0, installCmd);
454
572
  }
455
573
  }
456
- logger.box(nextSteps
457
- .map((step, i) => `${i + 1}. ${chalk.hex('#FFE600FF')(step)}`)
458
- .join('\n'), '🚀 Get started');
574
+ logger.box(nextSteps.map((step, i) => `${i + 1}. ${chalk.hex("#FFE600FF")(step)}`).join("\n"), "🚀 Get started");
459
575
  logger.newLine();
460
- logger.log('Happy coding! 🎨✨');
576
+ logger.log("Happy coding! 🎨✨");
461
577
  logger.newLine();
462
578
  }
579
+ async function addProject(projectName, options) {
580
+ const logger = new Logger(options.verbose);
581
+ logger.banner();
582
+ // Validate project name
583
+ if (!PROJECTS[projectName]) {
584
+ logger.error(`Invalid project: ${projectName}`);
585
+ logger.info(`Available projects: ${Object.keys(PROJECTS).join(", ")}`);
586
+ process.exit(1);
587
+ }
588
+ // Check if we're in a SumitApp project
589
+ const projectsDir = path.join(process.cwd(), "projects");
590
+ const packagesDir = path.join(process.cwd(), "packages");
591
+ if (!(await fs.pathExists(projectsDir)) || !(await fs.pathExists(packagesDir))) {
592
+ logger.error("This does not appear to be a SumitApp project.");
593
+ logger.info("Run this command from the root of your SumitApp project.");
594
+ process.exit(1);
595
+ }
596
+ // Check if project already exists
597
+ const targetPath = path.join(projectsDir, projectName);
598
+ if (await fs.pathExists(targetPath)) {
599
+ logger.error(`Project "${projectName}" already exists.`);
600
+ process.exit(1);
601
+ }
602
+ // Download the project
603
+ const spinner = ora({
604
+ text: `Adding ${projectName} project...`,
605
+ spinner: "dots12",
606
+ }).start();
607
+ try {
608
+ const project = PROJECTS[projectName];
609
+ const repoMatch = BASE_TEMPLATE.url.match(/github\.com[\/:]([\w-]+)\/([\w-]+)/);
610
+ if (!repoMatch) {
611
+ throw new Error("Invalid GitHub repository URL");
612
+ }
613
+ const [, owner, repo] = repoMatch;
614
+ const repoPath = `${owner}/${repo.replace(".git", "")}/${project.path}`;
615
+ const emitter = degit(repoPath, {
616
+ cache: false,
617
+ force: true,
618
+ verbose: options.verbose,
619
+ });
620
+ await emitter.clone(targetPath);
621
+ spinner.succeed(chalk.green(`Added ${projectName} project successfully!`));
622
+ // Update package.json if adding mobile (needs expo-dev-menu resolution)
623
+ if (projectName === "mobile") {
624
+ const packageJsonPath = path.join(process.cwd(), "package.json");
625
+ if (await fs.pathExists(packageJsonPath)) {
626
+ const packageJson = await fs.readJson(packageJsonPath);
627
+ if (!packageJson.resolutions) {
628
+ packageJson.resolutions = {};
629
+ }
630
+ if (!packageJson.resolutions["expo-dev-menu"]) {
631
+ packageJson.resolutions["expo-dev-menu"] = "^7.0.10";
632
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
633
+ logger.info("Updated package.json with expo-dev-menu resolution");
634
+ }
635
+ }
636
+ }
637
+ logger.newLine();
638
+ logger.info("Next steps:");
639
+ logger.info(" 1. Run: bun install");
640
+ logger.info(" 2. Run: bun dev");
641
+ }
642
+ catch (error) {
643
+ spinner.fail(chalk.red(`Failed to add ${projectName} project`));
644
+ logger.error(error.message);
645
+ process.exit(1);
646
+ }
647
+ }
463
648
  // CLI Setup
464
649
  const program = new Command();
465
650
  const require = createRequire(import.meta.url);
466
- const packageJson = require('../package.json');
651
+ const packageJson = require("../package.json");
467
652
  const CLI_VERSION = packageJson.version;
468
653
  program
469
- .name('create-sumit-app')
654
+ .name("create-sumit-app")
470
655
  .description("✨ A beautiful CLI to bootstrap projects from Sumit.app's project templates")
471
656
  .version(CLI_VERSION)
472
- .argument('[project-name]', 'The name of the project to create')
473
- .option('-t, --template <template>', 'Template to use (default, react-native, nextjs, minimal)')
474
- .option('-p, --package-manager <manager>', 'Package manager to use (npm, yarn, pnpm, bun)')
475
- .option('-v, --verbose', 'Enable verbose logging')
476
- // .option('--no-git', 'Skip git repository initialization')
477
- .option('--skip-install', 'Skip dependency installation')
657
+ .argument("[project-name]", "The name of the project to create")
658
+ .option("-p, --preset <preset>", "Preset to use (default, mobile-and-backend, website-and-backend, custom)")
659
+ .option("--projects <projects>", "Comma-separated list of projects (website, mobile, backend)")
660
+ .option("-m, --package-manager <manager>", "Package manager to use (bun)")
661
+ .option("-v, --verbose", "Enable verbose logging")
662
+ .option("--skip-install", "Skip dependency installation")
663
+ .option("-t, --template <template>", "[DEPRECATED] Use --preset instead")
478
664
  .action(async (projectName, options) => {
665
+ // Parse projects option if provided
666
+ if (options.projects) {
667
+ options.projects = options.projects.split(",").map((p) => p.trim());
668
+ }
479
669
  await createProject(projectName, options);
480
670
  });
671
+ // Add command for adding projects later
672
+ program
673
+ .command("add <project>")
674
+ .description("Add a project to an existing SumitApp setup (website, mobile, backend)")
675
+ .option("-v, --verbose", "Enable verbose logging")
676
+ .action(async (projectName, options) => {
677
+ await addProject(projectName, options);
678
+ });
481
679
  // Config command
482
680
  program
483
- .command('config')
484
- .description('Manage CLI configuration')
485
- .option('-l, --list', 'List current configuration')
486
- .option('-s, --set <key=value>', 'Set configuration value')
487
- .option('-r, --reset', 'Reset configuration to defaults')
681
+ .command("config")
682
+ .description("Manage CLI configuration")
683
+ .option("-l, --list", "List current configuration")
684
+ .option("-s, --set <key=value>", "Set configuration value")
685
+ .option("-r, --reset", "Reset configuration to defaults")
488
686
  .action(async (options) => {
489
687
  const logger = new Logger();
490
688
  if (options.list) {
@@ -493,67 +691,73 @@ program
493
691
  }
494
692
  if (options.reset) {
495
693
  await resetConfig();
496
- logger.success('Configuration reset to defaults');
694
+ logger.success("Configuration reset to defaults");
497
695
  return;
498
696
  }
499
697
  if (options.set) {
500
- const [key, value] = options.set.split('=');
698
+ const [key, value] = options.set.split("=");
501
699
  if (!key || value === undefined) {
502
- logger.error('Invalid format. Use: --set key=value');
700
+ logger.error("Invalid format. Use: --set key=value");
503
701
  return;
504
702
  }
505
- // Validate configuration keys
506
- const validKeys = [
507
- 'defaultTemplate',
508
- 'packageManager',
509
- // 'git',
510
- 'verbose',
511
- 'skipUpdateCheck',
512
- ];
703
+ const validKeys = ["defaultPreset", "packageManager", "verbose", "skipUpdateCheck"];
513
704
  if (!validKeys.includes(key)) {
514
705
  logger.error(`Invalid configuration key: ${key}`);
515
- logger.info(`Valid keys: ${validKeys.join(', ')}`);
706
+ logger.info(`Valid keys: ${validKeys.join(", ")}`);
516
707
  return;
517
708
  }
518
- // Parse boolean values
519
709
  let parsedValue = value;
520
- if (value === 'true' || value === 'false') {
521
- parsedValue = value === 'true';
710
+ if (value === "true" || value === "false") {
711
+ parsedValue = value === "true";
522
712
  }
523
713
  await updateConfig(key, parsedValue);
524
714
  logger.success(`Configuration updated: ${chalk.cyan(key)} = ${chalk.green(String(parsedValue))}`);
525
715
  logger.info(`Config file: ${chalk.gray(getConfigPath())}`);
526
716
  }
527
717
  });
528
- // Templates command
718
+ // Presets command
719
+ program
720
+ .command("presets")
721
+ .description("List available presets")
722
+ .action(() => {
723
+ listPresets();
724
+ });
725
+ // Projects command
726
+ program
727
+ .command("projects")
728
+ .description("List available projects")
729
+ .action(() => {
730
+ listProjects();
731
+ });
732
+ // Templates command (legacy, for backward compatibility)
529
733
  program
530
- .command('templates')
531
- .description('List available templates')
734
+ .command("templates")
735
+ .description('[DEPRECATED] List available templates (use "presets" instead)')
532
736
  .action(() => {
533
737
  listTemplates();
534
738
  });
535
739
  // Info command
536
740
  program
537
- .command('info')
538
- .description('Display environment info')
741
+ .command("info")
742
+ .description("Display environment info")
539
743
  .action(async () => {
540
744
  const logger = new Logger();
541
- const packageJson = await fs.readJson(path.join(__dirname, '..', 'package.json'));
745
+ const packageJson = await fs.readJson(path.join(__dirname, "..", "package.json"));
542
746
  logger.banner();
543
747
  logger.box(`Version: ${packageJson.version}\n` +
544
748
  `Node: ${process.version}\n` +
545
749
  `Platform: ${process.platform}\n` +
546
750
  `Architecture: ${process.arch}\n` +
547
- `Config: ${getConfigPath()}`, '🔍 Environment Info');
751
+ `Config: ${getConfigPath()}`, "🔍 Environment Info");
548
752
  });
549
753
  // Handle uncaught errors
550
- process.on('uncaughtException', (error) => {
754
+ process.on("uncaughtException", (error) => {
551
755
  const logger = new Logger();
552
756
  logger.error(`Unexpected error: ${error.message}`);
553
- logger.debug(error.stack || '');
757
+ logger.debug(error.stack || "");
554
758
  process.exit(1);
555
759
  });
556
- process.on('unhandledRejection', (reason, promise) => {
760
+ process.on("unhandledRejection", (reason, promise) => {
557
761
  const logger = new Logger();
558
762
  logger.error(`Unhandled rejection at: ${promise}, reason: ${reason}`);
559
763
  process.exit(1);