projxo 1.0.1 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projxo",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Cross-platform CLI tool to quickly create React, Next.js, Angular, and React Native projects with automatic IDE integration",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -44,6 +44,7 @@
44
44
  "homepage": "https://github.com/sasangachathumal/ProjXO#readme",
45
45
  "files": [
46
46
  "index.js",
47
+ "src/**/*",
47
48
  "README.md",
48
49
  "LICENSE"
49
50
  ],
package/src/cli.js ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Main CLI orchestration
3
+ * Coordinates the project creation workflow
4
+ */
5
+
6
+ const inquirer = require('inquirer');
7
+ const logger = require('./utils/logger');
8
+ const { pathExists } = require('./utils/fileSystem');
9
+ const { getProjectType } = require('./config/projectTypes');
10
+ const {
11
+ getProjectTypePrompt,
12
+ getProjectNamePrompt,
13
+ getDirectoryPrompt,
14
+ getIDEPrompt,
15
+ getOverwritePrompt
16
+ } = require('./prompts/questions');
17
+ const {
18
+ createProject,
19
+ displaySuccessMessage,
20
+ validateProjectParams
21
+ } = require('./handlers/projectCreator');
22
+ const { openInIDE } = require('./handlers/ideOpener');
23
+
24
+ /**
25
+ * Main CLI function
26
+ * Orchestrates the entire project creation flow
27
+ */
28
+ async function run() {
29
+ try {
30
+ // Display welcome banner
31
+ displayBanner();
32
+
33
+ // Get user inputs through prompts
34
+ const answers = await getUserInputs();
35
+
36
+ // Validate inputs
37
+ const validation = validateProjectParams(answers);
38
+ if (!validation.valid) {
39
+ logger.error(validation.error);
40
+ process.exit(1);
41
+ }
42
+
43
+ // Check if project exists and handle overwrite
44
+ const shouldProceed = await handleExistingProject(answers);
45
+ if (!shouldProceed) {
46
+ logger.warning('Operation cancelled');
47
+ process.exit(0);
48
+ }
49
+
50
+ // Create the project
51
+ const projectPath = await createProject({
52
+ projectType: answers.projectType,
53
+ projectName: answers.projectName,
54
+ directory: answers.directory
55
+ });
56
+
57
+ // Display success message
58
+ displaySuccessMessage(projectPath, answers.projectType);
59
+
60
+ // Open in IDE if selected
61
+ if (answers.selectedIDE !== 'skip') {
62
+ await openInIDE(projectPath, answers.selectedIDE);
63
+ }
64
+
65
+ } catch (error) {
66
+ handleError(error);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Display welcome banner
72
+ */
73
+ function displayBanner() {
74
+ logger.newLine();
75
+ logger.log('═══════════════════════════════════════════════════', 'bright');
76
+ logger.log(' ProjXO - Quick Project Setup', 'brightCyan');
77
+ logger.log('═══════════════════════════════════════════════════', 'bright');
78
+ logger.newLine();
79
+ }
80
+
81
+ /**
82
+ * Get user inputs through interactive prompts
83
+ * @returns {Promise<Object>} User answers
84
+ */
85
+ async function getUserInputs() {
86
+ const defaultDirectory = process.cwd();
87
+
88
+ // Get project type
89
+ const { projectType } = await inquirer.prompt([getProjectTypePrompt()]);
90
+
91
+ const config = getProjectType(projectType);
92
+ logger.success(`Selected: ${config.name}`);
93
+ logger.newLine();
94
+
95
+ // Get project name
96
+ const { projectName } = await inquirer.prompt([getProjectNamePrompt()]);
97
+
98
+ // Get directory
99
+ const { directory } = await inquirer.prompt([getDirectoryPrompt(defaultDirectory)]);
100
+
101
+ // Get IDE choice
102
+ const { selectedIDE } = await inquirer.prompt([getIDEPrompt()]);
103
+
104
+ return {
105
+ projectType,
106
+ projectName,
107
+ directory,
108
+ selectedIDE
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Handle existing project scenario
114
+ * @param {Object} answers - User answers containing projectName and directory
115
+ * @returns {Promise<boolean>} True if should proceed, false if cancelled
116
+ */
117
+ async function handleExistingProject({ projectName, directory }) {
118
+ const { expandHomePath, getProjectPath } = require('./utils/fileSystem');
119
+ const expandedDir = expandHomePath(directory);
120
+ const fullPath = getProjectPath(expandedDir, projectName);
121
+
122
+ if (pathExists(fullPath)) {
123
+ logger.warning(`Project "${projectName}" already exists at: ${fullPath}`);
124
+
125
+ const { overwrite } = await inquirer.prompt([getOverwritePrompt(projectName)]);
126
+
127
+ if (!overwrite) {
128
+ return false;
129
+ }
130
+
131
+ logger.info('Proceeding with overwrite...');
132
+ }
133
+
134
+ return true;
135
+ }
136
+
137
+ /**
138
+ * Handle errors gracefully
139
+ * @param {Error} error - Error object
140
+ */
141
+ function handleError(error) {
142
+ logger.newLine();
143
+
144
+ if (error.isTtyError) {
145
+ logger.error('Prompt could not be rendered in this environment');
146
+ logger.info('Please ensure you are running in an interactive terminal');
147
+ } else if (error.message === 'PROJECT_EXISTS') {
148
+ // This shouldn't happen as we handle it, but just in case
149
+ logger.error('Project already exists');
150
+ } else {
151
+ logger.error(`An error occurred: ${error.message}`);
152
+
153
+ // Show stack trace in debug mode
154
+ if (process.env.DEBUG) {
155
+ console.error(error);
156
+ }
157
+ }
158
+
159
+ logger.newLine();
160
+ process.exit(1);
161
+ }
162
+
163
+ /**
164
+ * Handle graceful shutdown on SIGINT (Ctrl+C)
165
+ */
166
+ process.on('SIGINT', () => {
167
+ logger.newLine();
168
+ logger.warning('Operation cancelled by user');
169
+ logger.newLine();
170
+ process.exit(0);
171
+ });
172
+
173
+ module.exports = { run };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * IDE/Editor configurations
3
+ * Defines supported IDEs and their command-line commands
4
+ *
5
+ * To add a new IDE:
6
+ * 1. Add a new key to IDES object
7
+ * 2. Specify name and command
8
+ * 3. Optionally add installation instructions in getIDEInstallInstructions
9
+ */
10
+
11
+ /**
12
+ * IDE configuration object
13
+ * @typedef {Object} IDEConfig
14
+ * @property {string} name - Display name for the IDE
15
+ * @property {string|null} command - Command-line command to open IDE (null for 'skip')
16
+ * @property {string} [description] - Optional description
17
+ */
18
+
19
+ const IDES = {
20
+ 'vscode': {
21
+ name: 'VS Code',
22
+ command: 'code',
23
+ description: 'Visual Studio Code'
24
+ },
25
+
26
+ 'cursor': {
27
+ name: 'Cursor',
28
+ command: 'cursor',
29
+ description: 'Cursor AI Editor'
30
+ },
31
+
32
+ 'webstorm': {
33
+ name: 'WebStorm',
34
+ command: 'webstorm',
35
+ description: 'JetBrains WebStorm IDE'
36
+ },
37
+
38
+ 'idea': {
39
+ name: 'IntelliJ IDEA',
40
+ command: 'idea',
41
+ description: 'JetBrains IntelliJ IDEA'
42
+ },
43
+
44
+ 'sublime': {
45
+ name: 'Sublime Text',
46
+ command: 'subl',
47
+ description: 'Sublime Text Editor'
48
+ },
49
+
50
+ 'atom': {
51
+ name: 'Atom',
52
+ command: 'atom',
53
+ description: 'GitHub Atom Editor'
54
+ },
55
+
56
+ 'skip': {
57
+ name: 'Skip (open manually)',
58
+ command: null,
59
+ description: 'Do not open in any IDE'
60
+ }
61
+ };
62
+
63
+ /**
64
+ * Get IDE configuration
65
+ * @param {string} ideKey - IDE key
66
+ * @returns {IDEConfig|null} Configuration object or null if not found
67
+ */
68
+ function getIDE(ideKey) {
69
+ return IDES[ideKey] || null;
70
+ }
71
+
72
+ /**
73
+ * Get all IDEs as an array
74
+ * @returns {Array<{key: string, config: IDEConfig}>}
75
+ */
76
+ function getAllIDEs() {
77
+ return Object.entries(IDES).map(([key, config]) => ({
78
+ key,
79
+ config
80
+ }));
81
+ }
82
+
83
+ /**
84
+ * Get formatted choices for inquirer prompts
85
+ * @returns {Array<{name: string, value: string}>}
86
+ */
87
+ function getIDEChoices() {
88
+ return Object.entries(IDES).map(([key, config]) => ({
89
+ name: config.name,
90
+ value: key,
91
+ short: config.name
92
+ }));
93
+ }
94
+
95
+ /**
96
+ * Check if an IDE key is valid
97
+ * @param {string} ideKey - IDE key to check
98
+ * @returns {boolean}
99
+ */
100
+ function isValidIDE(ideKey) {
101
+ return ideKey in IDES;
102
+ }
103
+
104
+ /**
105
+ * Get installation instructions for an IDE command
106
+ * @param {string} ideKey - IDE key
107
+ * @returns {string} Installation instructions
108
+ */
109
+ function getIDEInstallInstructions(ideKey) {
110
+ const instructions = {
111
+ 'vscode': 'Install "Shell Command: Install \'code\' command in PATH" from Command Palette (Cmd/Ctrl+Shift+P)',
112
+ 'cursor': 'Cursor command is usually available after installation',
113
+ 'webstorm': 'Enable in WebStorm: Tools → Create Command-line Launcher',
114
+ 'idea': 'Enable in IntelliJ IDEA: Tools → Create Command-line Launcher',
115
+ 'sublime': 'Create symlink: ln -s "/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl" /usr/local/bin/subl',
116
+ 'atom': 'Install shell commands from Atom: Atom → Install Shell Commands'
117
+ };
118
+
119
+ return instructions[ideKey] || 'Please refer to your IDE\'s documentation for command-line setup';
120
+ }
121
+
122
+ module.exports = {
123
+ IDES,
124
+ getIDE,
125
+ getAllIDEs,
126
+ getIDEChoices,
127
+ isValidIDE,
128
+ getIDEInstallInstructions
129
+ };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Project type configurations
3
+ * Defines all supported project types and their creation commands
4
+ *
5
+ * To add a new project type:
6
+ * 1. Add a new key to PROJECT_TYPES object
7
+ * 2. Specify name, command, getArgs function, and postInstall flag
8
+ * 3. Add corresponding help text to getNextSteps function
9
+ */
10
+
11
+ /**
12
+ * Project type configuration object
13
+ * @typedef {Object} ProjectTypeConfig
14
+ * @property {string} name - Display name for the project type
15
+ * @property {string} command - Command to execute (npm, npx, etc.)
16
+ * @property {Function} getArgs - Function that returns command arguments
17
+ * @property {boolean} postInstall - Whether to run npm install after creation
18
+ * @property {string} [description] - Optional description
19
+ */
20
+
21
+ const PROJECT_TYPES = {
22
+ 'react-vite': {
23
+ name: 'React + Vite',
24
+ description: 'React with Vite bundler (JavaScript)',
25
+ command: 'npm',
26
+ getArgs: (name) => ['create', 'vite@latest', name, '--', '--template', 'react'],
27
+ postInstall: true
28
+ },
29
+
30
+ 'react-vite-ts': {
31
+ name: 'React + Vite (TypeScript)',
32
+ description: 'React with Vite bundler and TypeScript',
33
+ command: 'npm',
34
+ getArgs: (name) => ['create', 'vite@latest', name, '--', '--template', 'react-ts'],
35
+ postInstall: true
36
+ },
37
+
38
+ 'nextjs': {
39
+ name: 'Next.js',
40
+ description: 'React framework for production',
41
+ command: 'npx',
42
+ getArgs: (name) => ['create-next-app@latest', name],
43
+ postInstall: false // create-next-app already installs dependencies
44
+ },
45
+
46
+ 'angular': {
47
+ name: 'Angular',
48
+ description: 'Platform for building web applications',
49
+ command: 'npx',
50
+ getArgs: (name) => ['@angular/cli@latest', 'new', name],
51
+ postInstall: false // Angular CLI already installs dependencies
52
+ },
53
+
54
+ 'react-native': {
55
+ name: 'React Native (Expo)',
56
+ description: 'Build native mobile apps with React',
57
+ command: 'npx',
58
+ getArgs: (name) => ['create-expo-app', name],
59
+ postInstall: false // Expo already installs dependencies
60
+ }
61
+ };
62
+
63
+ /**
64
+ * Get project type configuration
65
+ * @param {string} typeKey - Project type key
66
+ * @returns {ProjectTypeConfig|null} Configuration object or null if not found
67
+ */
68
+ function getProjectType(typeKey) {
69
+ return PROJECT_TYPES[typeKey] || null;
70
+ }
71
+
72
+ /**
73
+ * Get all project types as an array
74
+ * @returns {Array<{key: string, config: ProjectTypeConfig}>}
75
+ */
76
+ function getAllProjectTypes() {
77
+ return Object.entries(PROJECT_TYPES).map(([key, config]) => ({
78
+ key,
79
+ config
80
+ }));
81
+ }
82
+
83
+ /**
84
+ * Get formatted choices for inquirer prompts
85
+ * @returns {Array<{name: string, value: string}>}
86
+ */
87
+ function getProjectTypeChoices() {
88
+ return Object.entries(PROJECT_TYPES).map(([key, config]) => ({
89
+ name: `${config.name} (${key})`,
90
+ value: key,
91
+ short: config.name
92
+ }));
93
+ }
94
+
95
+ /**
96
+ * Get next steps instructions based on project type
97
+ * @param {string} typeKey - Project type key
98
+ * @param {string} projectPath - Full path to the project
99
+ * @returns {string[]} Array of instruction strings
100
+ */
101
+ function getNextSteps(typeKey, projectPath) {
102
+ const steps = [`cd ${projectPath}`];
103
+
104
+ switch (typeKey) {
105
+ case 'react-vite':
106
+ case 'react-vite-ts':
107
+ steps.push('npm run dev');
108
+ break;
109
+
110
+ case 'nextjs':
111
+ steps.push('npm run dev');
112
+ break;
113
+
114
+ case 'angular':
115
+ steps.push('ng serve');
116
+ break;
117
+
118
+ case 'react-native':
119
+ steps.push('npx expo start');
120
+ break;
121
+
122
+ default:
123
+ steps.push('npm start');
124
+ }
125
+
126
+ return steps;
127
+ }
128
+
129
+ /**
130
+ * Check if a project type exists
131
+ * @param {string} typeKey - Project type key to check
132
+ * @returns {boolean}
133
+ */
134
+ function isValidProjectType(typeKey) {
135
+ return typeKey in PROJECT_TYPES;
136
+ }
137
+
138
+ module.exports = {
139
+ PROJECT_TYPES,
140
+ getProjectType,
141
+ getAllProjectTypes,
142
+ getProjectTypeChoices,
143
+ getNextSteps,
144
+ isValidProjectType
145
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * IDE opener handler
3
+ * Handles opening projects in various IDEs
4
+ */
5
+
6
+ const { runCommand } = require('../utils/command');
7
+ const { getIDE, getIDEInstallInstructions } = require('../config/ides');
8
+ const logger = require('../utils/logger');
9
+
10
+ /**
11
+ * Open a project in the specified IDE
12
+ * @param {string} projectPath - Full path to the project
13
+ * @param {string} ideKey - IDE key (e.g., 'vscode', 'cursor')
14
+ * @returns {Promise<boolean>} True if opened successfully, false otherwise
15
+ */
16
+ async function openInIDE(projectPath, ideKey) {
17
+ // Get IDE configuration
18
+ const ide = getIDE(ideKey);
19
+
20
+ // Skip if no IDE selected or invalid IDE
21
+ if (!ide || !ide.command) {
22
+ return false;
23
+ }
24
+
25
+ try {
26
+ logger.info(`Opening project in ${ide.name}...`);
27
+
28
+ // Execute IDE command with project path
29
+ await runCommand(ide.command, [projectPath]);
30
+
31
+ logger.success(`Project opened in ${ide.name}`);
32
+ return true;
33
+
34
+ } catch (error) {
35
+ // Handle IDE command failure
36
+ logger.error(`Could not open ${ide.name}`);
37
+ logger.warning(`Please ensure ${ide.name} is installed and command-line tools are enabled`);
38
+
39
+ // Provide installation instructions
40
+ const instructions = getIDEInstallInstructions(ideKey);
41
+ logger.log(`\n Setup: ${instructions}`, 'dim');
42
+
43
+ // Provide fallback option
44
+ logger.log(` Or manually open: ${projectPath}`, 'yellow');
45
+
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Validate if IDE command is available
52
+ * @param {string} ideKey - IDE key to validate
53
+ * @returns {Promise<boolean>} True if command is available
54
+ */
55
+ async function validateIDECommand(ideKey) {
56
+ const ide = getIDE(ideKey);
57
+
58
+ if (!ide || !ide.command) {
59
+ return true; // Skip validation for 'skip' option
60
+ }
61
+
62
+ try {
63
+ const { commandExists } = require('../utils/command');
64
+ return await commandExists(ide.command);
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ module.exports = {
71
+ openInIDE,
72
+ validateIDECommand
73
+ };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Project creator handler
3
+ * Handles the project creation process
4
+ */
5
+
6
+ const { runCommand } = require('../utils/command');
7
+ const {
8
+ expandHomePath,
9
+ ensureDirectory,
10
+ pathExists,
11
+ getProjectPath
12
+ } = require('../utils/fileSystem');
13
+ const { getProjectType, getNextSteps } = require('../config/projectTypes');
14
+ const logger = require('../utils/logger');
15
+
16
+ /**
17
+ * Create a new project
18
+ * @param {Object} options - Project creation options
19
+ * @param {string} options.projectType - Project type key
20
+ * @param {string} options.projectName - Name of the project
21
+ * @param {string} options.directory - Directory path where project will be created
22
+ * @returns {Promise<string>} Full path to the created project
23
+ * @throws {Error} If project creation fails
24
+ */
25
+ async function createProject({ projectType, projectName, directory }) {
26
+ // Get project type configuration
27
+ const config = getProjectType(projectType);
28
+
29
+ if (!config) {
30
+ throw new Error(`Invalid project type: ${projectType}`);
31
+ }
32
+
33
+ // Expand home directory and ensure directory exists
34
+ const expandedDir = expandHomePath(directory);
35
+ ensureDirectory(expandedDir);
36
+
37
+ // Get full project path
38
+ const fullPath = getProjectPath(expandedDir, projectName);
39
+
40
+ // Check if project already exists
41
+ if (pathExists(fullPath)) {
42
+ throw new Error('PROJECT_EXISTS');
43
+ }
44
+
45
+ // Display creation start message
46
+ logger.section('Starting project creation...');
47
+
48
+ try {
49
+ // Execute project creation command in the target directory
50
+ await runCommand(
51
+ config.command,
52
+ config.getArgs(projectName),
53
+ expandedDir
54
+ );
55
+
56
+ // Run post-install if needed
57
+ if (config.postInstall) {
58
+ logger.info('Installing dependencies...');
59
+ await runCommand('npm', ['install'], fullPath);
60
+ }
61
+
62
+ return fullPath;
63
+
64
+ } catch (error) {
65
+ // Provide helpful error message
66
+ throw new Error(`Failed to create project: ${error.message}`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Display project creation success message with next steps
72
+ * @param {string} projectPath - Full path to the created project
73
+ * @param {string} projectType - Project type key
74
+ */
75
+ function displaySuccessMessage(projectPath, projectType) {
76
+ logger.section('✓ Project created successfully!');
77
+
78
+ logger.log('Project location:', 'bright');
79
+ logger.log(` ${projectPath}`, 'cyan');
80
+
81
+ logger.newLine();
82
+ logger.log('Next steps:', 'bright');
83
+
84
+ const steps = getNextSteps(projectType, projectPath);
85
+ steps.forEach(step => {
86
+ logger.log(` ${step}`, 'green');
87
+ });
88
+
89
+ logger.newLine();
90
+ }
91
+
92
+ /**
93
+ * Validate project creation parameters
94
+ * @param {Object} params - Parameters to validate
95
+ * @param {string} params.projectType - Project type key
96
+ * @param {string} params.projectName - Project name
97
+ * @param {string} params.directory - Directory path
98
+ * @returns {Object} { valid: boolean, error?: string }
99
+ */
100
+ function validateProjectParams({ projectType, projectName, directory }) {
101
+ // Validate project type
102
+ const config = getProjectType(projectType);
103
+ if (!config) {
104
+ return {
105
+ valid: false,
106
+ error: `Invalid project type: ${projectType}`
107
+ };
108
+ }
109
+
110
+ // Validate project name
111
+ if (!projectName || projectName.trim() === '') {
112
+ return {
113
+ valid: false,
114
+ error: 'Project name is required'
115
+ };
116
+ }
117
+
118
+ // Validate directory
119
+ if (!directory || directory.trim() === '') {
120
+ return {
121
+ valid: false,
122
+ error: 'Directory is required'
123
+ };
124
+ }
125
+
126
+ return { valid: true };
127
+ }
128
+
129
+ module.exports = {
130
+ createProject,
131
+ displaySuccessMessage,
132
+ validateProjectParams
133
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Inquirer prompt definitions
3
+ * Centralizes all user prompts for consistency
4
+ */
5
+
6
+ const { getProjectTypeChoices } = require('../config/projectTypes');
7
+ const { getIDEChoices } = require('../config/ides');
8
+ const { validateProjectName } = require('../utils/fileSystem');
9
+
10
+ /**
11
+ * Get project type selection prompt
12
+ * @returns {Object} Inquirer prompt configuration
13
+ */
14
+ function getProjectTypePrompt() {
15
+ return {
16
+ type: 'list',
17
+ name: 'projectType',
18
+ message: 'Select project type:',
19
+ choices: getProjectTypeChoices(),
20
+ pageSize: 10
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Get project name input prompt
26
+ * @returns {Object} Inquirer prompt configuration
27
+ */
28
+ function getProjectNamePrompt() {
29
+ return {
30
+ type: 'input',
31
+ name: 'projectName',
32
+ message: 'Enter project name:',
33
+ validate: (input) => {
34
+ const result = validateProjectName(input);
35
+ return result.valid ? true : result.error;
36
+ },
37
+ filter: (input) => input.trim() // Remove leading/trailing whitespace
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Get directory path input prompt
43
+ * @param {string} defaultPath - Default directory path
44
+ * @returns {Object} Inquirer prompt configuration
45
+ */
46
+ function getDirectoryPrompt(defaultPath) {
47
+ return {
48
+ type: 'input',
49
+ name: 'directory',
50
+ message: 'Enter directory path:',
51
+ default: defaultPath,
52
+ filter: (input) => input.trim()
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Get IDE selection prompt
58
+ * @returns {Object} Inquirer prompt configuration
59
+ */
60
+ function getIDEPrompt() {
61
+ return {
62
+ type: 'list',
63
+ name: 'selectedIDE',
64
+ message: 'Select IDE to open:',
65
+ choices: getIDEChoices(),
66
+ pageSize: 10
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Get overwrite confirmation prompt
72
+ * @param {string} projectName - Name of the project that already exists
73
+ * @returns {Object} Inquirer prompt configuration
74
+ */
75
+ function getOverwritePrompt(projectName) {
76
+ return {
77
+ type: 'confirm',
78
+ name: 'overwrite',
79
+ message: `Project "${projectName}" already exists. Overwrite?`,
80
+ default: false
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Get all prompts for project creation flow
86
+ * @param {string} defaultDirectory - Default directory path
87
+ * @returns {Array<Object>} Array of prompt configurations
88
+ */
89
+ function getProjectCreationPrompts(defaultDirectory) {
90
+ return [
91
+ getProjectTypePrompt(),
92
+ getProjectNamePrompt(),
93
+ getDirectoryPrompt(defaultDirectory),
94
+ getIDEPrompt()
95
+ ];
96
+ }
97
+
98
+ module.exports = {
99
+ getProjectTypePrompt,
100
+ getProjectNamePrompt,
101
+ getDirectoryPrompt,
102
+ getIDEPrompt,
103
+ getOverwritePrompt,
104
+ getProjectCreationPrompts
105
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Command execution utility
3
+ * Handles spawning child processes and command execution
4
+ */
5
+
6
+ const { spawn } = require('child_process');
7
+ const logger = require('./logger');
8
+
9
+ /**
10
+ * Execute a shell command
11
+ * @param {string} command - Command to execute (e.g., 'npm', 'npx')
12
+ * @param {string[]} args - Array of command arguments
13
+ * @param {string} [cwd] - Working directory for command execution
14
+ * @returns {Promise<void>} Resolves when command completes successfully
15
+ * @throws {Error} If command exits with non-zero code
16
+ */
17
+ function runCommand(command, args, cwd) {
18
+ return new Promise((resolve, reject) => {
19
+ // Log the command being executed for debugging
20
+ logger.log(`\nExecuting: ${command} ${args.join(' ')}`, 'cyan');
21
+
22
+ // Spawn the process
23
+ const proc = spawn(command, args, {
24
+ stdio: 'inherit', // Pipe stdin, stdout, stderr to parent process
25
+ shell: true, // Use shell for command execution
26
+ cwd: cwd || process.cwd() // Use provided cwd or current directory
27
+ });
28
+
29
+ // Handle process completion
30
+ proc.on('close', (code) => {
31
+ if (code !== 0) {
32
+ reject(new Error(`Command failed with exit code ${code}`));
33
+ } else {
34
+ resolve();
35
+ }
36
+ });
37
+
38
+ // Handle process errors (e.g., command not found)
39
+ proc.on('error', (err) => {
40
+ reject(new Error(`Failed to execute command: ${err.message}`));
41
+ });
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Check if a command exists in the system PATH
47
+ * @param {string} command - Command to check
48
+ * @returns {Promise<boolean>} True if command exists
49
+ */
50
+ async function commandExists(command) {
51
+ const checkCmd = process.platform === 'win32' ? 'where' : 'which';
52
+
53
+ try {
54
+ await runCommand(checkCmd, [command], process.cwd());
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ module.exports = {
62
+ runCommand,
63
+ commandExists
64
+ };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * File system utility functions
3
+ * Handles all file and directory operations
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+ const logger = require('./logger');
10
+
11
+ /**
12
+ * Expand tilde (~) in path to home directory
13
+ * @param {string} filePath - Path that may contain ~
14
+ * @returns {string} Expanded absolute path
15
+ * @example
16
+ * expandHomePath('~/Documents') // -> '/Users/username/Documents'
17
+ */
18
+ function expandHomePath(filePath) {
19
+ if (filePath.startsWith('~')) {
20
+ return filePath.replace('~', os.homedir());
21
+ }
22
+ return filePath;
23
+ }
24
+
25
+ /**
26
+ * Ensure a directory exists, create if it doesn't
27
+ * @param {string} dirPath - Directory path to ensure
28
+ * @returns {boolean} True if directory was created, false if already existed
29
+ */
30
+ function ensureDirectory(dirPath) {
31
+ const expandedPath = expandHomePath(dirPath);
32
+
33
+ if (!fs.existsSync(expandedPath)) {
34
+ fs.mkdirSync(expandedPath, { recursive: true });
35
+ logger.success(`Created directory: ${expandedPath}`);
36
+ return true;
37
+ }
38
+
39
+ return false;
40
+ }
41
+
42
+ /**
43
+ * Check if a path exists
44
+ * @param {string} filePath - Path to check
45
+ * @returns {boolean} True if path exists
46
+ */
47
+ function pathExists(filePath) {
48
+ return fs.existsSync(expandHomePath(filePath));
49
+ }
50
+
51
+ /**
52
+ * Check if a path is a directory
53
+ * @param {string} dirPath - Path to check
54
+ * @returns {boolean} True if path exists and is a directory
55
+ */
56
+ function isDirectory(dirPath) {
57
+ try {
58
+ const stats = fs.statSync(expandHomePath(dirPath));
59
+ return stats.isDirectory();
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Get the full absolute path
67
+ * @param {string} dirPath - Directory path
68
+ * @param {string} projectName - Project name
69
+ * @returns {string} Full project path
70
+ */
71
+ function getProjectPath(dirPath, projectName) {
72
+ const expandedDir = expandHomePath(dirPath);
73
+ return path.join(expandedDir, projectName);
74
+ }
75
+
76
+ /**
77
+ * Validate directory path
78
+ * @param {string} dirPath - Directory path to validate
79
+ * @returns {Object} { valid: boolean, error?: string }
80
+ */
81
+ function validateDirectoryPath(dirPath) {
82
+ const expandedPath = expandHomePath(dirPath);
83
+
84
+ // Check if path contains invalid characters
85
+ const invalidChars = /[<>:"|?*]/;
86
+ if (invalidChars.test(expandedPath)) {
87
+ return {
88
+ valid: false,
89
+ error: 'Path contains invalid characters'
90
+ };
91
+ }
92
+
93
+ // Check if parent directory exists (if path doesn't exist)
94
+ if (!pathExists(expandedPath)) {
95
+ const parentDir = path.dirname(expandedPath);
96
+ if (!pathExists(parentDir)) {
97
+ return {
98
+ valid: false,
99
+ error: 'Parent directory does not exist'
100
+ };
101
+ }
102
+ }
103
+
104
+ return { valid: true };
105
+ }
106
+
107
+ /**
108
+ * Validate project name
109
+ * @param {string} projectName - Project name to validate
110
+ * @returns {Object} { valid: boolean, error?: string }
111
+ */
112
+ function validateProjectName(projectName) {
113
+ if (!projectName || !projectName.trim()) {
114
+ return {
115
+ valid: false,
116
+ error: 'Project name cannot be empty'
117
+ };
118
+ }
119
+
120
+ // Allow letters, numbers, hyphens, underscores, dots
121
+ if (!/^[a-zA-Z0-9-_.]+$/.test(projectName)) {
122
+ return {
123
+ valid: false,
124
+ error: 'Project name can only contain letters, numbers, hyphens, underscores, and dots'
125
+ };
126
+ }
127
+
128
+ // Don't allow names starting with dots or hyphens
129
+ if (/^[.-]/.test(projectName)) {
130
+ return {
131
+ valid: false,
132
+ error: 'Project name cannot start with a dot or hyphen'
133
+ };
134
+ }
135
+
136
+ return { valid: true };
137
+ }
138
+
139
+ module.exports = {
140
+ expandHomePath,
141
+ ensureDirectory,
142
+ pathExists,
143
+ isDirectory,
144
+ getProjectPath,
145
+ validateDirectoryPath,
146
+ validateProjectName
147
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Logger utility for colored terminal output
3
+ * Provides consistent formatting across the application
4
+ */
5
+
6
+ // ANSI color codes for terminal styling
7
+ const COLORS = {
8
+ reset: '\x1b[0m',
9
+ bright: '\x1b[1m',
10
+ dim: '\x1b[2m',
11
+
12
+ // Standard colors
13
+ black: '\x1b[30m',
14
+ red: '\x1b[31m',
15
+ green: '\x1b[32m',
16
+ yellow: '\x1b[33m',
17
+ blue: '\x1b[34m',
18
+ magenta: '\x1b[35m',
19
+ cyan: '\x1b[36m',
20
+ white: '\x1b[37m',
21
+
22
+ // Bright colors
23
+ brightRed: '\x1b[91m',
24
+ brightGreen: '\x1b[92m',
25
+ brightYellow: '\x1b[93m',
26
+ brightBlue: '\x1b[94m',
27
+ brightCyan: '\x1b[96m'
28
+ };
29
+
30
+ /**
31
+ * Main logging function
32
+ * @param {string} message - Message to log
33
+ * @param {string} color - Color key from COLORS object
34
+ */
35
+ function log(message, color = 'reset') {
36
+ const colorCode = COLORS[color] || COLORS.reset;
37
+ console.log(`${colorCode}${message}${COLORS.reset}`);
38
+ }
39
+
40
+ /**
41
+ * Log success message with green checkmark
42
+ * @param {string} message - Success message
43
+ */
44
+ function success(message) {
45
+ log(`✓ ${message}`, 'green');
46
+ }
47
+
48
+ /**
49
+ * Log error message with red X
50
+ * @param {string} message - Error message
51
+ */
52
+ function error(message) {
53
+ log(`✗ ${message}`, 'red');
54
+ }
55
+
56
+ /**
57
+ * Log warning message with yellow exclamation
58
+ * @param {string} message - Warning message
59
+ */
60
+ function warning(message) {
61
+ log(`⚠ ${message}`, 'yellow');
62
+ }
63
+
64
+ /**
65
+ * Log info message with blue icon
66
+ * @param {string} message - Info message
67
+ */
68
+ function info(message) {
69
+ log(`ℹ ${message}`, 'blue');
70
+ }
71
+
72
+ /**
73
+ * Log a horizontal separator line
74
+ * @param {number} length - Length of separator (default: 50)
75
+ * @param {string} color - Color of separator
76
+ */
77
+ function separator(length = 50, color = 'bright') {
78
+ log('='.repeat(length), color);
79
+ }
80
+
81
+ /**
82
+ * Log a section header with separators
83
+ * @param {string} title - Section title
84
+ */
85
+ function section(title) {
86
+ console.log(); // Empty line
87
+ separator();
88
+ log(title, 'bright');
89
+ separator();
90
+ console.log(); // Empty line
91
+ }
92
+
93
+ /**
94
+ * Log an empty line
95
+ */
96
+ function newLine() {
97
+ console.log();
98
+ }
99
+
100
+ module.exports = {
101
+ log,
102
+ success,
103
+ error,
104
+ warning,
105
+ info,
106
+ separator,
107
+ section,
108
+ newLine,
109
+ COLORS
110
+ };