projxo 1.0.2 → 1.2.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,76 @@
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.2.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 { recentCommand } = require('./src/commands/recent');
16
+ const { openCommand } = require('./src/commands/open');
17
+ const logger = require('./src/utils/logger');
15
18
 
16
- // Run the CLI
17
- run().catch((error) => {
18
- console.error('Fatal error:', error.message);
19
- process.exit(1);
20
- });
19
+ // Package info
20
+ const packageJson = require('./package.json');
21
+
22
+ // Configure CLI
23
+ program
24
+ .name('pxo')
25
+ .description('Quick project setup and management for modern web frameworks')
26
+ .version(packageJson.version);
27
+
28
+ // Default command (no arguments) - Create new project
29
+ program
30
+ .action(() => {
31
+ createProject();
32
+ });
33
+
34
+ // List all projects
35
+ program
36
+ .command('list')
37
+ .alias('ls')
38
+ .description('List all tracked projects')
39
+ .action(() => {
40
+ listCommand();
41
+ });
42
+
43
+ // Show recent projects
44
+ program
45
+ .command('recent')
46
+ .description('Show recently accessed projects')
47
+ .argument('[limit]', 'number of projects to show', '10')
48
+ .action((limit) => {
49
+ recentCommand(parseInt(limit));
50
+ });
51
+
52
+ // Open project
53
+ program
54
+ .command('open [project-name]')
55
+ .description('Open a project in IDE')
56
+ .action((projectName) => {
57
+ openCommand(projectName);
58
+ });
59
+
60
+ // Handle errors
61
+ program.exitOverride();
62
+
63
+ try {
64
+ program.parse(process.argv);
65
+ } catch (error) {
66
+ if (error.code === 'commander.help') {
67
+ // Help was displayed, exit normally
68
+ process.exit(0);
69
+ } else if (error.code === 'commander.version') {
70
+ // Version was displayed, exit normally
71
+ process.exit(0);
72
+ } else {
73
+ // logger.error(`Error: ${error.message}`);
74
+ process.exit(1);
75
+ }
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projxo",
3
- "version": "1.0.2",
3
+ "version": "1.2.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": {
@@ -55,6 +55,7 @@
55
55
  "win32"
56
56
  ],
57
57
  "dependencies": {
58
- "inquirer": "^8.2.6"
58
+ "inquirer": "^8.2.6",
59
+ "commander": "^11.1.0"
59
60
  }
60
61
  }
package/src/cli.js CHANGED
@@ -60,6 +60,17 @@ async function run() {
60
60
  // Open in IDE if selected
61
61
  if (answers.selectedIDE !== 'skip') {
62
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');
63
74
  }
64
75
 
65
76
  } catch (error) {
@@ -0,0 +1,252 @@
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).padEnd(16);
71
+ const timeAgo = formatRelativeTime(project.lastAccessed).padEnd(15);
72
+ const nameDisplay = project.name.padEnd(30);
73
+
74
+ return {
75
+ name: `${nameDisplay} ${typeDisplay} ${timeAgo}`,
76
+ value: project.id,
77
+ short: project.name
78
+ };
79
+ });
80
+
81
+ // Add separator and action options
82
+ choices.push(
83
+ new inquirer.Separator(),
84
+ { name: '← Back', value: 'back' }
85
+ );
86
+
87
+ const { selectedId } = await inquirer.prompt([
88
+ {
89
+ type: 'list',
90
+ name: 'selectedId',
91
+ message: 'Select a project:',
92
+ choices,
93
+ pageSize: 15
94
+ }
95
+ ]);
96
+
97
+ if (selectedId === 'back') {
98
+ return;
99
+ }
100
+
101
+ // Show actions for selected project
102
+ await showProjectActions(selectedId);
103
+
104
+ } catch (error) {
105
+ if (error.isTtyError) {
106
+ logger.error('This command requires an interactive terminal');
107
+ } else {
108
+ logger.error(`Failed to list projects: ${error.message}`);
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Show actions for a selected project
115
+ * @param {string} projectId - Project ID
116
+ */
117
+ async function showProjectActions(projectId) {
118
+ const { getProjectById } = require('../storage/projects');
119
+ const project = getProjectById(projectId);
120
+
121
+ if (!project) {
122
+ logger.error('Project not found');
123
+ return;
124
+ }
125
+
126
+ const { action } = await inquirer.prompt([
127
+ {
128
+ type: 'list',
129
+ name: 'action',
130
+ message: `Actions for "${project.name}":`,
131
+ choices: [
132
+ { name: '📂 Open in IDE', value: 'open' },
133
+ { name: '📋 Copy path', value: 'copy' },
134
+ { name: '🗑️ Remove from tracking', value: 'delete' },
135
+ { name: 'ℹ️ Show details', value: 'details' },
136
+ new inquirer.Separator(),
137
+ { name: '← Back to list', value: 'back' }
138
+ ]
139
+ }
140
+ ]);
141
+
142
+ switch (action) {
143
+ case 'open':
144
+ await handleOpenProject(project);
145
+ break;
146
+
147
+ case 'copy':
148
+ handleCopyPath(project);
149
+ break;
150
+
151
+ case 'delete':
152
+ await handleDeleteProject(project);
153
+ await listCommand(); // Refresh list
154
+ break;
155
+
156
+ case 'details':
157
+ showProjectDetails(project);
158
+ await showProjectActions(projectId); // Show actions again
159
+ break;
160
+
161
+ case 'back':
162
+ await listCommand(); // Go back to list
163
+ break;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Handle opening project in IDE
169
+ * @param {Object} project - Project object
170
+ */
171
+ async function handleOpenProject(project) {
172
+ // Update last accessed time
173
+ touchProject(project.id);
174
+
175
+ // Use project's preferred IDE or prompt
176
+ let ideKey = project.ide;
177
+
178
+ if (!ideKey || ideKey === 'skip') {
179
+ const { getIDEChoices } = require('../config/ides');
180
+ const { selectedIDE } = await inquirer.prompt([
181
+ {
182
+ type: 'list',
183
+ name: 'selectedIDE',
184
+ message: 'Select IDE:',
185
+ choices: getIDEChoices()
186
+ }
187
+ ]);
188
+ ideKey = selectedIDE;
189
+ }
190
+
191
+ if (ideKey !== 'skip') {
192
+ const success = await openInIDE(project.path, ideKey);
193
+ if (success) {
194
+ logger.success(`Opened ${project.name}`);
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Handle copying project path
201
+ * @param {Object} project - Project object
202
+ */
203
+ function handleCopyPath(project) {
204
+ // For now, just display the path
205
+ // In future, could use clipboard library
206
+ logger.info('Project path:');
207
+ logger.log(` ${project.path}`, 'cyan');
208
+ logger.log('\n(Copy from above)', 'dim');
209
+ }
210
+
211
+ /**
212
+ * Handle deleting project from tracking
213
+ * @param {Object} project - Project object
214
+ */
215
+ async function handleDeleteProject(project) {
216
+ const { confirm } = await inquirer.prompt([
217
+ {
218
+ type: 'confirm',
219
+ name: 'confirm',
220
+ message: `Remove "${project.name}" from tracking? (Files won't be deleted)`,
221
+ default: false
222
+ }
223
+ ]);
224
+
225
+ if (confirm) {
226
+ deleteProject(project.id);
227
+ logger.success(`Removed ${project.name} from tracking`);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Show detailed project information
233
+ * @param {Object} project - Project object
234
+ */
235
+ function showProjectDetails(project) {
236
+ logger.newLine();
237
+ logger.log('━'.repeat(50), 'dim');
238
+ logger.log(` ${project.name}`, 'bright');
239
+ logger.log('━'.repeat(50), 'dim');
240
+ logger.log(` Type: ${getTypeDisplay(project.type)}`, 'cyan');
241
+ logger.log(` Path: ${project.path}`, 'dim');
242
+ logger.log(` Created: ${new Date(project.createdAt).toLocaleString()}`, 'dim');
243
+ logger.log(` Last accessed: ${formatRelativeTime(project.lastAccessed)}`, 'dim');
244
+ if (project.ide) {
245
+ const ide = getIDE(project.ide);
246
+ logger.log(` Default IDE: ${ide?.name || project.ide}`, 'dim');
247
+ }
248
+ logger.log('━'.repeat(50), 'dim');
249
+ logger.newLine();
250
+ }
251
+
252
+ module.exports = { listCommand };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Open command - Quick open project by name
3
+ * Usage: pxo open <project-name>
4
+ */
5
+
6
+ const inquirer = require('inquirer');
7
+ const { getProjectByName, touchProject, searchProjects } = require('../storage/projects');
8
+ const { openInIDE } = require('../handlers/ideOpener');
9
+ const { getIDEChoices } = require('../config/ides');
10
+ const logger = require('../utils/logger');
11
+
12
+ /**
13
+ * Execute open command
14
+ * @param {string} projectName - Project name to open
15
+ */
16
+ async function openCommand(projectName) {
17
+ try {
18
+ if (!projectName) {
19
+ logger.error('Please provide a project name');
20
+ logger.log('\nUsage:', 'dim');
21
+ logger.log(' pxo open <project-name>', 'cyan');
22
+ return;
23
+ }
24
+
25
+ // Try to find project
26
+ let project = getProjectByName(projectName);
27
+
28
+ // If not found, try fuzzy search
29
+ if (!project) {
30
+ const matches = searchProjects(projectName);
31
+
32
+ if (matches.length === 0) {
33
+ logger.error(`Project "${projectName}" not found`);
34
+ logger.log('\nList all projects with:', 'dim');
35
+ logger.log(' pxo list', 'cyan');
36
+ return;
37
+ }
38
+
39
+ if (matches.length === 1) {
40
+ project = matches[0];
41
+ logger.info(`Found similar project: ${project.name}`);
42
+ } else {
43
+ // Multiple matches, let user choose
44
+ project = await selectFromMatches(matches);
45
+ if (!project) return;
46
+ }
47
+ }
48
+
49
+ // Update last accessed time
50
+ touchProject(project.id);
51
+
52
+ // Determine which IDE to use
53
+ let ideKey = project.ide;
54
+
55
+ if (!ideKey || ideKey === 'skip') {
56
+ const { selectedIDE } = await inquirer.prompt([
57
+ {
58
+ type: 'list',
59
+ name: 'selectedIDE',
60
+ message: `Open "${project.name}" in:`,
61
+ choices: getIDEChoices()
62
+ }
63
+ ]);
64
+ ideKey = selectedIDE;
65
+ }
66
+
67
+ // Open in IDE
68
+ if (ideKey !== 'skip') {
69
+ const success = await openInIDE(project.path, ideKey);
70
+ if (success) {
71
+ logger.success(`Opened ${project.name}`);
72
+ }
73
+ } else {
74
+ logger.info(`Project path: ${project.path}`);
75
+ }
76
+
77
+ } catch (error) {
78
+ logger.error(`Failed to open project: ${error.message}`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Let user select from multiple matches
84
+ * @param {Array} matches - Array of matching projects
85
+ * @returns {Promise<Object|null>} Selected project or null
86
+ */
87
+ async function selectFromMatches(matches) {
88
+ logger.info(`Found ${matches.length} matching projects:`);
89
+ logger.newLine();
90
+
91
+ const choices = matches.map(p => ({
92
+ name: `${p.name} (${p.type})`,
93
+ value: p.id,
94
+ short: p.name
95
+ }));
96
+
97
+ choices.push(
98
+ new inquirer.Separator(),
99
+ { name: '← Cancel', value: 'cancel' }
100
+ );
101
+
102
+ const { selectedId } = await inquirer.prompt([
103
+ {
104
+ type: 'list',
105
+ name: 'selectedId',
106
+ message: 'Which project did you mean?',
107
+ choices
108
+ }
109
+ ]);
110
+
111
+ if (selectedId === 'cancel') {
112
+ return null;
113
+ }
114
+
115
+ const { getProjectById } = require('../storage/projects');
116
+ return getProjectById(selectedId);
117
+ }
118
+
119
+ module.exports = { openCommand };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Recent command - Show recently accessed projects
3
+ * Usage: pxo recent [limit]
4
+ */
5
+
6
+ const inquirer = require('inquirer');
7
+ const { getRecentProjects, touchProject } = require('../storage/projects');
8
+ const { openInIDE } = require('../handlers/ideOpener');
9
+ const logger = require('../utils/logger');
10
+
11
+ /**
12
+ * Format relative time (same as list.js)
13
+ * @param {string} isoDate - ISO date string
14
+ * @returns {string} Human-readable relative time
15
+ */
16
+ function formatRelativeTime(isoDate) {
17
+ const date = new Date(isoDate);
18
+ const now = new Date();
19
+ const diffMs = now - date;
20
+ const diffMins = Math.floor(diffMs / 60000);
21
+ const diffHours = Math.floor(diffMs / 3600000);
22
+ const diffDays = Math.floor(diffMs / 86400000);
23
+
24
+ if (diffMins < 1) return 'just now';
25
+ if (diffMins < 60) return `${diffMins} min${diffMins > 1 ? 's' : ''} ago`;
26
+ if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
27
+ if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
28
+ if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`;
29
+ return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`;
30
+ }
31
+
32
+ /**
33
+ * Get project type display name
34
+ * @param {string} type - Project type key
35
+ * @returns {string} Formatted display name
36
+ */
37
+ function getTypeDisplay(type) {
38
+ const typeMap = {
39
+ 'react-vite': 'React+Vite',
40
+ 'react-vite-ts': 'React+Vite(TS)',
41
+ 'nextjs': 'Next.js',
42
+ 'angular': 'Angular',
43
+ 'react-native': 'React Native'
44
+ };
45
+ return typeMap[type] || type;
46
+ }
47
+
48
+ /**
49
+ * Execute recent command
50
+ * @param {number} limit - Number of recent projects to show
51
+ */
52
+ async function recentCommand(limit = 10) {
53
+ try {
54
+ const projects = getRecentProjects(limit);
55
+
56
+ if (projects.length === 0) {
57
+ logger.info('No recent projects found');
58
+ logger.log('\nCreate your first project with:', 'dim');
59
+ logger.log(' pxo', 'cyan');
60
+ return;
61
+ }
62
+
63
+ logger.newLine();
64
+ logger.log(`🕐 Recent Projects (${projects.length})`, 'bright');
65
+ logger.newLine();
66
+
67
+ // Create choices for inquirer
68
+ const choices = projects.map((project, index) => {
69
+ const typeDisplay = getTypeDisplay(project.type).padEnd(16);
70
+ const timeAgo = formatRelativeTime(project.lastAccessed).padEnd(15);
71
+ const indexDisplay = `${index + 1}.`.padEnd(4);
72
+ const nameDisplay = project.name.padEnd(30);
73
+
74
+ return {
75
+ name: `${indexDisplay}${nameDisplay} ${typeDisplay} ${timeAgo}`,
76
+ value: project.id,
77
+ short: project.name
78
+ };
79
+ });
80
+
81
+ // Add separator and back option
82
+ choices.push(
83
+ new inquirer.Separator(),
84
+ { name: '← Cancel', value: 'cancel' }
85
+ );
86
+
87
+ const { selectedId } = await inquirer.prompt([
88
+ {
89
+ type: 'list',
90
+ name: 'selectedId',
91
+ message: 'Select a project to open:',
92
+ choices,
93
+ pageSize: 15
94
+ }
95
+ ]);
96
+
97
+ if (selectedId === 'cancel') {
98
+ return;
99
+ }
100
+
101
+ // Open selected project
102
+ await openSelectedProject(selectedId);
103
+
104
+ } catch (error) {
105
+ if (error.isTtyError) {
106
+ logger.error('This command requires an interactive terminal');
107
+ } else {
108
+ logger.error(`Failed to show recent projects: ${error.message}`);
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Open selected project in IDE
115
+ * @param {string} projectId - Project ID
116
+ */
117
+ async function openSelectedProject(projectId) {
118
+ const { getProjectById } = require('../storage/projects');
119
+ const { getIDEChoices } = require('../config/ides');
120
+
121
+ const project = getProjectById(projectId);
122
+
123
+ if (!project) {
124
+ logger.error('Project not found');
125
+ return;
126
+ }
127
+
128
+ // Update last accessed time
129
+ touchProject(project.id);
130
+
131
+ // Use project's preferred IDE or prompt
132
+ let ideKey = project.ide;
133
+
134
+ if (!ideKey || ideKey === 'skip') {
135
+ const { selectedIDE } = await inquirer.prompt([
136
+ {
137
+ type: 'list',
138
+ name: 'selectedIDE',
139
+ message: 'Select IDE:',
140
+ choices: getIDEChoices()
141
+ }
142
+ ]);
143
+ ideKey = selectedIDE;
144
+ }
145
+
146
+ if (ideKey !== 'skip') {
147
+ const success = await openInIDE(project.path, ideKey);
148
+ if (success) {
149
+ logger.success(`Opened ${project.name}`);
150
+ }
151
+ } else {
152
+ logger.info(`Project path: ${project.path}`);
153
+ }
154
+ }
155
+
156
+ module.exports = { recentCommand };
@@ -59,6 +59,24 @@ async function createProject({ projectType, projectName, directory }) {
59
59
  await runCommand('npm', ['install'], fullPath);
60
60
  }
61
61
 
62
+ // Add project to tracking database
63
+ const { addProject } = require('../storage/projects');
64
+ try {
65
+ addProject({
66
+ name: projectName,
67
+ path: fullPath,
68
+ type: projectType,
69
+ ide: null // Will be set when opened
70
+ });
71
+ logger.success('Project added to ProjXO tracking');
72
+ } catch (dbError) {
73
+ // Don't fail project creation if database save fails
74
+ logger.error(`Failed to add project to tracking database: ${dbError.message}`);
75
+ if (process.env.DEBUG) {
76
+ console.error(dbError);
77
+ }
78
+ }
79
+
62
80
  return fullPath;
63
81
 
64
82
  } catch (error) {