projxo 1.0.1 → 1.1.0

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/index.js CHANGED
@@ -1,20 +1,57 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * ProjXO - Quick Project Setup CLI
4
+ * ProjXO - Quick Project Setup & Management CLI
5
5
  *
6
6
  * Entry point for the CLI application
7
- * This file should remain minimal, with all logic in src/cli.js
7
+ * Handles command routing and argument parsing
8
8
  *
9
- * @author Sasanga Chathumal
10
- * @version 1.0.1
9
+ * @version 1.1.0
11
10
  */
12
11
 
13
- // Import the main CLI function
14
- const { run } = require('./src/cli');
12
+ const { program } = require('commander');
13
+ const { run: createProject } = require('./src/cli');
14
+ const { listCommand } = require('./src/commands/list');
15
+ const logger = require('./src/utils/logger');
15
16
 
16
- // Run the CLI
17
- run().catch((error) => {
18
- console.error('Fatal error:', error.message);
19
- process.exit(1);
20
- });
17
+ // Package info
18
+ const packageJson = require('./package.json');
19
+
20
+ // Configure CLI
21
+ program
22
+ .name('pxo')
23
+ .description('Quick project setup and management for modern web frameworks')
24
+ .version(packageJson.version);
25
+
26
+ // Default command (no arguments) - Create new project
27
+ program
28
+ .action(() => {
29
+ createProject();
30
+ });
31
+
32
+ // List all projects
33
+ program
34
+ .command('list')
35
+ .alias('ls')
36
+ .description('List all tracked projects')
37
+ .action(() => {
38
+ listCommand();
39
+ });
40
+
41
+ // Handle errors
42
+ program.exitOverride();
43
+
44
+ try {
45
+ program.parse(process.argv);
46
+ } catch (error) {
47
+ if (error.code === 'commander.help') {
48
+ // Help was displayed, exit normally
49
+ process.exit(0);
50
+ } else if (error.code === 'commander.version') {
51
+ // Version was displayed, exit normally
52
+ process.exit(0);
53
+ } else {
54
+ // logger.error(`Error: ${error.message}`);
55
+ process.exit(1);
56
+ }
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projxo",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
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
  ],
@@ -54,6 +55,7 @@
54
55
  "win32"
55
56
  ],
56
57
  "dependencies": {
57
- "inquirer": "^8.2.6"
58
+ "inquirer": "^8.2.6",
59
+ "commander": "^11.1.0"
58
60
  }
59
61
  }
package/src/cli.js ADDED
@@ -0,0 +1,184 @@
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
+ // Update project with IDE preference
65
+ const { getProjectByPath, updateProject } = require('./storage/projects');
66
+ const project = getProjectByPath(projectPath);
67
+ if (project) {
68
+ updateProject(project.id, { ide: answers.selectedIDE });
69
+ }
70
+ } else {
71
+ // Show quick access info
72
+ logger.log('\nQuick access:', 'dim');
73
+ logger.log(` pxo list`, 'cyan');
74
+ }
75
+
76
+ } catch (error) {
77
+ handleError(error);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Display welcome banner
83
+ */
84
+ function displayBanner() {
85
+ logger.newLine();
86
+ logger.log('═══════════════════════════════════════════════════', 'bright');
87
+ logger.log(' ProjXO - Quick Project Setup', 'brightCyan');
88
+ logger.log('═══════════════════════════════════════════════════', 'bright');
89
+ logger.newLine();
90
+ }
91
+
92
+ /**
93
+ * Get user inputs through interactive prompts
94
+ * @returns {Promise<Object>} User answers
95
+ */
96
+ async function getUserInputs() {
97
+ const defaultDirectory = process.cwd();
98
+
99
+ // Get project type
100
+ const { projectType } = await inquirer.prompt([getProjectTypePrompt()]);
101
+
102
+ const config = getProjectType(projectType);
103
+ logger.success(`Selected: ${config.name}`);
104
+ logger.newLine();
105
+
106
+ // Get project name
107
+ const { projectName } = await inquirer.prompt([getProjectNamePrompt()]);
108
+
109
+ // Get directory
110
+ const { directory } = await inquirer.prompt([getDirectoryPrompt(defaultDirectory)]);
111
+
112
+ // Get IDE choice
113
+ const { selectedIDE } = await inquirer.prompt([getIDEPrompt()]);
114
+
115
+ return {
116
+ projectType,
117
+ projectName,
118
+ directory,
119
+ selectedIDE
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Handle existing project scenario
125
+ * @param {Object} answers - User answers containing projectName and directory
126
+ * @returns {Promise<boolean>} True if should proceed, false if cancelled
127
+ */
128
+ async function handleExistingProject({ projectName, directory }) {
129
+ const { expandHomePath, getProjectPath } = require('./utils/fileSystem');
130
+ const expandedDir = expandHomePath(directory);
131
+ const fullPath = getProjectPath(expandedDir, projectName);
132
+
133
+ if (pathExists(fullPath)) {
134
+ logger.warning(`Project "${projectName}" already exists at: ${fullPath}`);
135
+
136
+ const { overwrite } = await inquirer.prompt([getOverwritePrompt(projectName)]);
137
+
138
+ if (!overwrite) {
139
+ return false;
140
+ }
141
+
142
+ logger.info('Proceeding with overwrite...');
143
+ }
144
+
145
+ return true;
146
+ }
147
+
148
+ /**
149
+ * Handle errors gracefully
150
+ * @param {Error} error - Error object
151
+ */
152
+ function handleError(error) {
153
+ logger.newLine();
154
+
155
+ if (error.isTtyError) {
156
+ logger.error('Prompt could not be rendered in this environment');
157
+ logger.info('Please ensure you are running in an interactive terminal');
158
+ } else if (error.message === 'PROJECT_EXISTS') {
159
+ // This shouldn't happen as we handle it, but just in case
160
+ logger.error('Project already exists');
161
+ } else {
162
+ logger.error(`An error occurred: ${error.message}`);
163
+
164
+ // Show stack trace in debug mode
165
+ if (process.env.DEBUG) {
166
+ console.error(error);
167
+ }
168
+ }
169
+
170
+ logger.newLine();
171
+ process.exit(1);
172
+ }
173
+
174
+ /**
175
+ * Handle graceful shutdown on SIGINT (Ctrl+C)
176
+ */
177
+ process.on('SIGINT', () => {
178
+ logger.newLine();
179
+ logger.warning('Operation cancelled by user');
180
+ logger.newLine();
181
+ process.exit(0);
182
+ });
183
+
184
+ module.exports = { run };
@@ -0,0 +1,251 @@
1
+ /**
2
+ * List command - Show all tracked projects
3
+ * Usage: pxo list
4
+ */
5
+
6
+ const inquirer = require('inquirer');
7
+ const path = require('path');
8
+ const { getAllProjects, touchProject, deleteProject } = require('../storage/projects');
9
+ const { openInIDE } = require('../handlers/ideOpener');
10
+ const { getIDE } = require('../config/ides');
11
+ const logger = require('../utils/logger');
12
+
13
+ /**
14
+ * Format relative time
15
+ * @param {string} isoDate - ISO date string
16
+ * @returns {string} Human-readable relative time
17
+ */
18
+ function formatRelativeTime(isoDate) {
19
+ const date = new Date(isoDate);
20
+ const now = new Date();
21
+ const diffMs = now - date;
22
+ const diffMins = Math.floor(diffMs / 60000);
23
+ const diffHours = Math.floor(diffMs / 3600000);
24
+ const diffDays = Math.floor(diffMs / 86400000);
25
+
26
+ if (diffMins < 1) return 'just now';
27
+ if (diffMins < 60) return `${diffMins} min${diffMins > 1 ? 's' : ''} ago`;
28
+ if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
29
+ if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
30
+ if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`;
31
+ return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`;
32
+ }
33
+
34
+ /**
35
+ * Get project type display name
36
+ * @param {string} type - Project type key
37
+ * @returns {string} Formatted display name
38
+ */
39
+ function getTypeDisplay(type) {
40
+ const typeMap = {
41
+ 'react-vite': 'React+Vite',
42
+ 'react-vite-ts': 'React+Vite(TS)',
43
+ 'nextjs': 'Next.js',
44
+ 'angular': 'Angular',
45
+ 'react-native': 'React Native'
46
+ };
47
+ return typeMap[type] || type;
48
+ }
49
+
50
+ /**
51
+ * Execute list command
52
+ */
53
+ async function listCommand() {
54
+ try {
55
+ const projects = getAllProjects();
56
+
57
+ if (projects.length === 0) {
58
+ logger.info('No projects found');
59
+ logger.log('\nCreate your first project with:', 'dim');
60
+ logger.log(' pxo', 'cyan');
61
+ return;
62
+ }
63
+
64
+ logger.newLine();
65
+ logger.log(`📦 Your Projects (${projects.length})`, 'bright');
66
+ logger.newLine();
67
+
68
+ // Create choices for inquirer
69
+ const choices = projects.map(project => {
70
+ const typeDisplay = getTypeDisplay(project.type);
71
+ const timeAgo = formatRelativeTime(project.lastAccessed);
72
+
73
+ return {
74
+ name: `${project.name} │ ${typeDisplay} │ ${timeAgo}`,
75
+ value: project.id,
76
+ short: project.name
77
+ };
78
+ });
79
+
80
+ // Add separator and action options
81
+ choices.push(
82
+ new inquirer.Separator(),
83
+ { name: '← Back', value: 'back' }
84
+ );
85
+
86
+ const { selectedId } = await inquirer.prompt([
87
+ {
88
+ type: 'list',
89
+ name: 'selectedId',
90
+ message: 'Select a project:',
91
+ choices,
92
+ pageSize: 15
93
+ }
94
+ ]);
95
+
96
+ if (selectedId === 'back') {
97
+ return;
98
+ }
99
+
100
+ // Show actions for selected project
101
+ await showProjectActions(selectedId);
102
+
103
+ } catch (error) {
104
+ if (error.isTtyError) {
105
+ logger.error('This command requires an interactive terminal');
106
+ } else {
107
+ logger.error(`Failed to list projects: ${error.message}`);
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Show actions for a selected project
114
+ * @param {string} projectId - Project ID
115
+ */
116
+ async function showProjectActions(projectId) {
117
+ const { getProjectById } = require('../storage/projects');
118
+ const project = getProjectById(projectId);
119
+
120
+ if (!project) {
121
+ logger.error('Project not found');
122
+ return;
123
+ }
124
+
125
+ const { action } = await inquirer.prompt([
126
+ {
127
+ type: 'list',
128
+ name: 'action',
129
+ message: `Actions for "${project.name}":`,
130
+ choices: [
131
+ { name: '📂 Open in IDE', value: 'open' },
132
+ { name: '📋 Copy path', value: 'copy' },
133
+ { name: '🗑️ Remove from tracking', value: 'delete' },
134
+ { name: 'ℹ️ Show details', value: 'details' },
135
+ new inquirer.Separator(),
136
+ { name: '← Back to list', value: 'back' }
137
+ ]
138
+ }
139
+ ]);
140
+
141
+ switch (action) {
142
+ case 'open':
143
+ await handleOpenProject(project);
144
+ break;
145
+
146
+ case 'copy':
147
+ handleCopyPath(project);
148
+ break;
149
+
150
+ case 'delete':
151
+ await handleDeleteProject(project);
152
+ await listCommand(); // Refresh list
153
+ break;
154
+
155
+ case 'details':
156
+ showProjectDetails(project);
157
+ await showProjectActions(projectId); // Show actions again
158
+ break;
159
+
160
+ case 'back':
161
+ await listCommand(); // Go back to list
162
+ break;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Handle opening project in IDE
168
+ * @param {Object} project - Project object
169
+ */
170
+ async function handleOpenProject(project) {
171
+ // Update last accessed time
172
+ touchProject(project.id);
173
+
174
+ // Use project's preferred IDE or prompt
175
+ let ideKey = project.ide;
176
+
177
+ if (!ideKey || ideKey === 'skip') {
178
+ const { getIDEChoices } = require('../config/ides');
179
+ const { selectedIDE } = await inquirer.prompt([
180
+ {
181
+ type: 'list',
182
+ name: 'selectedIDE',
183
+ message: 'Select IDE:',
184
+ choices: getIDEChoices()
185
+ }
186
+ ]);
187
+ ideKey = selectedIDE;
188
+ }
189
+
190
+ if (ideKey !== 'skip') {
191
+ const success = await openInIDE(project.path, ideKey);
192
+ if (success) {
193
+ logger.success(`Opened ${project.name}`);
194
+ }
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Handle copying project path
200
+ * @param {Object} project - Project object
201
+ */
202
+ function handleCopyPath(project) {
203
+ // For now, just display the path
204
+ // In future, could use clipboard library
205
+ logger.info('Project path:');
206
+ logger.log(` ${project.path}`, 'cyan');
207
+ logger.log('\n(Copy from above)', 'dim');
208
+ }
209
+
210
+ /**
211
+ * Handle deleting project from tracking
212
+ * @param {Object} project - Project object
213
+ */
214
+ async function handleDeleteProject(project) {
215
+ const { confirm } = await inquirer.prompt([
216
+ {
217
+ type: 'confirm',
218
+ name: 'confirm',
219
+ message: `Remove "${project.name}" from tracking? (Files won't be deleted)`,
220
+ default: false
221
+ }
222
+ ]);
223
+
224
+ if (confirm) {
225
+ deleteProject(project.id);
226
+ logger.success(`Removed ${project.name} from tracking`);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Show detailed project information
232
+ * @param {Object} project - Project object
233
+ */
234
+ function showProjectDetails(project) {
235
+ logger.newLine();
236
+ logger.log('━'.repeat(50), 'dim');
237
+ logger.log(` ${project.name}`, 'bright');
238
+ logger.log('━'.repeat(50), 'dim');
239
+ logger.log(` Type: ${getTypeDisplay(project.type)}`, 'cyan');
240
+ logger.log(` Path: ${project.path}`, 'dim');
241
+ logger.log(` Created: ${new Date(project.createdAt).toLocaleString()}`, 'dim');
242
+ logger.log(` Last accessed: ${formatRelativeTime(project.lastAccessed)}`, 'dim');
243
+ if (project.ide) {
244
+ const ide = getIDE(project.ide);
245
+ logger.log(` Default IDE: ${ide?.name || project.ide}`, 'dim');
246
+ }
247
+ logger.log('━'.repeat(50), 'dim');
248
+ logger.newLine();
249
+ }
250
+
251
+ module.exports = { listCommand };
@@ -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
+ };