projxo 1.0.2 → 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.2",
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": {
@@ -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,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 };
@@ -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) {
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Database utility for ProjXO
3
+ * Handles low-level file operations for project tracking
4
+ */
5
+
6
+ /**
7
+ * Storage implementation using JSON files.
8
+ *
9
+ * Future consideration: Migrate to SQLite for better performance
10
+ * at scale (1000+ projects). Node.js includes sqlite module
11
+ * since v22.5.0, making it dependency-free.
12
+ *
13
+ * Reasons for current JSON approach:
14
+ * - Human-readable and editable
15
+ * - Zero dependencies
16
+ * - Sufficient performance for typical use
17
+ * - Easier debugging and backup
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const os = require('os');
23
+
24
+ // Storage directory in user's home
25
+ const STORAGE_DIR = path.join(os.homedir(), '.projxo');
26
+ const PROJECTS_FILE = path.join(STORAGE_DIR, 'projects.json');
27
+ const CONFIG_FILE = path.join(STORAGE_DIR, 'config.json');
28
+
29
+ /**
30
+ * Initialize storage directory and files
31
+ * Creates directory and empty files if they don't exist
32
+ */
33
+ function initializeStorage() {
34
+ // Create storage directory if it doesn't exist
35
+ if (!fs.existsSync(STORAGE_DIR)) {
36
+ fs.mkdirSync(STORAGE_DIR, { recursive: true });
37
+ }
38
+
39
+ // Create projects file if it doesn't exist
40
+ if (!fs.existsSync(PROJECTS_FILE)) {
41
+ fs.writeFileSync(PROJECTS_FILE, JSON.stringify({ projects: [] }, null, 2), 'utf8');
42
+ }
43
+
44
+ // Create config file if it doesn't exist
45
+ if (!fs.existsSync(CONFIG_FILE)) {
46
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify({
47
+ version: '1.0.0',
48
+ defaultIDE: null,
49
+ lastSync: null
50
+ }, null, 2), 'utf8');
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Read projects database
56
+ * @returns {Object} Projects data object
57
+ */
58
+ function readProjects() {
59
+ try {
60
+ initializeStorage();
61
+ const data = fs.readFileSync(PROJECTS_FILE, 'utf8');
62
+ return JSON.parse(data);
63
+ } catch (error) {
64
+ // If file is corrupted, return empty structure
65
+ console.warn('Failed to read projects database, initializing new one');
66
+ return { projects: [] };
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Write projects database
72
+ * @param {Object} data - Projects data object
73
+ */
74
+ function writeProjects(data) {
75
+ try {
76
+ initializeStorage();
77
+ fs.writeFileSync(PROJECTS_FILE, JSON.stringify(data, null, 2), 'utf8');
78
+ } catch (error) {
79
+ throw new Error(`Failed to write projects database: ${error.message}`);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Read config
85
+ * @returns {Object} Config data object
86
+ */
87
+ function readConfig() {
88
+ try {
89
+ initializeStorage();
90
+ const data = fs.readFileSync(CONFIG_FILE, 'utf8');
91
+ return JSON.parse(data);
92
+ } catch (error) {
93
+ console.warn('Failed to read config, initializing new one');
94
+ return { version: '1.0.0', defaultIDE: null };
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Write config
100
+ * @param {Object} data - Config data object
101
+ */
102
+ function writeConfig(data) {
103
+ try {
104
+ initializeStorage();
105
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8');
106
+ } catch (error) {
107
+ throw new Error(`Failed to write config: ${error.message}`);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Get storage directory path
113
+ * @returns {string} Full path to storage directory
114
+ */
115
+ function getStorageDir() {
116
+ return STORAGE_DIR;
117
+ }
118
+
119
+ /**
120
+ * Backup database files
121
+ * Creates timestamped backups
122
+ * @returns {string} Backup directory path
123
+ */
124
+ function backupDatabase() {
125
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
126
+ const backupDir = path.join(STORAGE_DIR, 'backups', timestamp);
127
+
128
+ fs.mkdirSync(backupDir, { recursive: true });
129
+
130
+ if (fs.existsSync(PROJECTS_FILE)) {
131
+ fs.copyFileSync(PROJECTS_FILE, path.join(backupDir, 'projects.json'));
132
+ }
133
+
134
+ if (fs.existsSync(CONFIG_FILE)) {
135
+ fs.copyFileSync(CONFIG_FILE, path.join(backupDir, 'config.json'));
136
+ }
137
+
138
+ return backupDir;
139
+ }
140
+
141
+ module.exports = {
142
+ initializeStorage,
143
+ readProjects,
144
+ writeProjects,
145
+ readConfig,
146
+ writeConfig,
147
+ getStorageDir,
148
+ backupDatabase
149
+ };
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Debug script for ProjXO storage
5
+ * Run this to test if storage is working correctly
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ console.log('šŸ” ProjXO Storage Debug\n');
13
+
14
+ // Check storage directory
15
+ const STORAGE_DIR = path.join(os.homedir(), '.projxo');
16
+ console.log('1. Storage directory:', STORAGE_DIR);
17
+ console.log(' Exists?', fs.existsSync(STORAGE_DIR) ? 'āœ“' : 'āœ—');
18
+
19
+ if (fs.existsSync(STORAGE_DIR)) {
20
+ // Check permissions
21
+ try {
22
+ fs.accessSync(STORAGE_DIR, fs.constants.R_OK | fs.constants.W_OK);
23
+ console.log(' Writable? āœ“');
24
+ } catch (err) {
25
+ console.log(' Writable? āœ— (Permission denied)');
26
+ console.log(' Error:', err.message);
27
+ }
28
+
29
+ // List files
30
+ const files = fs.readdirSync(STORAGE_DIR);
31
+ console.log(' Files:', files.length > 0 ? files.join(', ') : '(empty)');
32
+ } else {
33
+ console.log(' Creating directory...');
34
+ try {
35
+ fs.mkdirSync(STORAGE_DIR, { recursive: true });
36
+ console.log(' āœ“ Created successfully');
37
+ } catch (err) {
38
+ console.log(' āœ— Failed to create:', err.message);
39
+ process.exit(1);
40
+ }
41
+ }
42
+
43
+ console.log('\n2. Testing storage module...');
44
+
45
+ try {
46
+ const { initializeStorage, readProjects, writeProjects } = require('./database');
47
+
48
+ // Initialize
49
+ console.log(' Initializing storage...');
50
+ initializeStorage();
51
+ console.log(' āœ“ Initialized');
52
+
53
+ // Check files again
54
+ const PROJECTS_FILE = path.join(STORAGE_DIR, 'projects.json');
55
+ const BOOKMARKS_FILE = path.join(STORAGE_DIR, 'bookmarks.json');
56
+ const CONFIG_FILE = path.join(STORAGE_DIR, 'config.json');
57
+
58
+ console.log('\n3. Checking files:');
59
+ console.log(' projects.json:', fs.existsSync(PROJECTS_FILE) ? 'āœ“' : 'āœ—');
60
+ console.log(' bookmarks.json:', fs.existsSync(BOOKMARKS_FILE) ? 'āœ“' : 'āœ—');
61
+ console.log(' config.json:', fs.existsSync(CONFIG_FILE) ? 'āœ“' : 'āœ—');
62
+
63
+ // Test read/write
64
+ console.log('\n4. Testing read/write:');
65
+ const data = readProjects();
66
+ console.log(' Read projects:', data.projects ? `āœ“ (${data.projects.length} projects)` : 'āœ—');
67
+
68
+ // Test adding a project
69
+ console.log('\n5. Testing addProject:');
70
+ const { addProject } = require('./projects');
71
+
72
+ const testProject = {
73
+ name: 'test-project-' + Date.now(),
74
+ path: '/tmp/test-project',
75
+ type: 'react-vite',
76
+ ide: null
77
+ };
78
+
79
+ const project = addProject(testProject);
80
+ console.log(' āœ“ Added test project:', project.name);
81
+
82
+ // Read back
83
+ const updatedData = readProjects();
84
+ console.log(' āœ“ Total projects now:', updatedData.projects.length);
85
+
86
+ // Show the file contents
87
+ console.log('\n6. projects.json contents:');
88
+ const content = fs.readFileSync(PROJECTS_FILE, 'utf8');
89
+ console.log(content);
90
+
91
+ console.log('\nāœ… All tests passed!');
92
+ console.log('\nYou can now run: pxo list');
93
+
94
+ } catch (error) {
95
+ console.log(' āœ— Error:', error.message);
96
+ console.log('\nFull error:');
97
+ console.error(error);
98
+ process.exit(1);
99
+ }