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.
@@ -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
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Projects storage management
3
+ * CRUD operations for project tracking
4
+ */
5
+
6
+ const { randomUUID } = require('crypto');
7
+ const fs = require('fs');
8
+ const { readProjects, writeProjects } = require('./database');
9
+
10
+ /**
11
+ * Add a new project to tracking
12
+ * @param {Object} projectData - Project information
13
+ * @param {string} projectData.name - Project name
14
+ * @param {string} projectData.path - Full path to project
15
+ * @param {string} projectData.type - Project type (react-vite, nextjs, etc.)
16
+ * @param {string} [projectData.ide] - IDE used to open
17
+ * @returns {Object} Created project object with ID
18
+ */
19
+ function addProject({ name, path, type, ide = null }) {
20
+ const data = readProjects();
21
+
22
+ // Check if project already exists
23
+ const existing = data.projects.find(p => p.path === path);
24
+ if (existing) {
25
+ // Update existing project
26
+ existing.lastAccessed = new Date().toISOString();
27
+ existing.type = type;
28
+ if (ide) existing.ide = ide;
29
+ writeProjects(data);
30
+ return existing;
31
+ }
32
+
33
+ // Create new project entry
34
+ const project = {
35
+ id: randomUUID(),
36
+ name,
37
+ path,
38
+ type,
39
+ createdAt: new Date().toISOString(),
40
+ lastAccessed: new Date().toISOString(),
41
+ ide: ide || null,
42
+ bookmarked: false,
43
+ tags: []
44
+ };
45
+
46
+ data.projects.push(project);
47
+ writeProjects(data);
48
+
49
+ return project;
50
+ }
51
+
52
+ /**
53
+ * Get all projects
54
+ * @param {Object} options - Filter options
55
+ * @param {boolean} [options.bookmarkedOnly] - Return only bookmarked projects
56
+ * @param {string} [options.type] - Filter by project type
57
+ * @returns {Array} Array of project objects
58
+ */
59
+ function getAllProjects({ bookmarkedOnly = false, type = null } = {}) {
60
+ const data = readProjects();
61
+ let projects = data.projects;
62
+
63
+ // Filter bookmarked
64
+ if (bookmarkedOnly) {
65
+ projects = projects.filter(p => p.bookmarked);
66
+ }
67
+
68
+ // Filter by type
69
+ if (type) {
70
+ projects = projects.filter(p => p.type === type);
71
+ }
72
+
73
+ // Sort by last accessed (most recent first)
74
+ projects.sort((a, b) =>
75
+ new Date(b.lastAccessed) - new Date(a.lastAccessed)
76
+ );
77
+
78
+ return projects;
79
+ }
80
+
81
+ /**
82
+ * Get project by name (fuzzy match)
83
+ * @param {string} name - Project name to search for
84
+ * @returns {Object|null} Project object or null if not found
85
+ */
86
+ function getProjectByName(name) {
87
+ const data = readProjects();
88
+
89
+ // Try exact match first
90
+ let project = data.projects.find(p =>
91
+ p.name.toLowerCase() === name.toLowerCase()
92
+ );
93
+
94
+ if (project) return project;
95
+
96
+ // Try partial match
97
+ project = data.projects.find(p =>
98
+ p.name.toLowerCase().includes(name.toLowerCase())
99
+ );
100
+
101
+ return project || null;
102
+ }
103
+
104
+ /**
105
+ * Get project by ID
106
+ * @param {string} id - Project ID
107
+ * @returns {Object|null} Project object or null if not found
108
+ */
109
+ function getProjectById(id) {
110
+ const data = readProjects();
111
+ return data.projects.find(p => p.id === id) || null;
112
+ }
113
+
114
+ /**
115
+ * Get project by path
116
+ * @param {string} path - Project path
117
+ * @returns {Object|null} Project object or null if not found
118
+ */
119
+ function getProjectByPath(path) {
120
+ const data = readProjects();
121
+ return data.projects.find(p => p.path === path) || null;
122
+ }
123
+
124
+ /**
125
+ * Update project
126
+ * @param {string} id - Project ID
127
+ * @param {Object} updates - Fields to update
128
+ * @returns {Object|null} Updated project or null if not found
129
+ */
130
+ function updateProject(id, updates) {
131
+ const data = readProjects();
132
+ const project = data.projects.find(p => p.id === id);
133
+
134
+ if (!project) return null;
135
+
136
+ // Update fields
137
+ Object.assign(project, updates);
138
+ project.lastAccessed = new Date().toISOString();
139
+
140
+ writeProjects(data);
141
+ return project;
142
+ }
143
+
144
+ /**
145
+ * Update project's last accessed time
146
+ * @param {string} id - Project ID
147
+ */
148
+ function touchProject(id) {
149
+ updateProject(id, { lastAccessed: new Date().toISOString() });
150
+ }
151
+
152
+ /**
153
+ * Delete project from tracking
154
+ * @param {string} id - Project ID
155
+ * @returns {boolean} True if deleted, false if not found
156
+ */
157
+ function deleteProject(id) {
158
+ const data = readProjects();
159
+ const index = data.projects.findIndex(p => p.id === id);
160
+
161
+ if (index === -1) return false;
162
+
163
+ data.projects.splice(index, 1);
164
+ writeProjects(data);
165
+
166
+ return true;
167
+ }
168
+
169
+ /**
170
+ * Get recently accessed projects
171
+ * @param {number} limit - Maximum number of projects to return
172
+ * @returns {Array} Array of recent project objects
173
+ */
174
+ function getRecentProjects(limit = 10) {
175
+ const data = readProjects();
176
+
177
+ return data.projects
178
+ .sort((a, b) => new Date(b.lastAccessed) - new Date(a.lastAccessed))
179
+ .slice(0, limit);
180
+ }
181
+
182
+ /**
183
+ * Search projects by query
184
+ * @param {string} query - Search query
185
+ * @returns {Array} Array of matching project objects
186
+ */
187
+ function searchProjects(query) {
188
+ const data = readProjects();
189
+ const lowerQuery = query.toLowerCase();
190
+
191
+ return data.projects.filter(p =>
192
+ p.name.toLowerCase().includes(lowerQuery) ||
193
+ p.type.toLowerCase().includes(lowerQuery) ||
194
+ p.path.toLowerCase().includes(lowerQuery) ||
195
+ (p.tags && p.tags.some(tag => tag.toLowerCase().includes(lowerQuery)))
196
+ );
197
+ }
198
+
199
+ /**
200
+ * Clean projects that no longer exist on disk
201
+ * @returns {Array} Array of removed project paths
202
+ */
203
+ function cleanProjects() {
204
+ const data = readProjects();
205
+ const removed = [];
206
+
207
+ data.projects = data.projects.filter(p => {
208
+ if (!fs.existsSync(p.path)) {
209
+ removed.push(p.path);
210
+ return false;
211
+ }
212
+ return true;
213
+ });
214
+
215
+ if (removed.length > 0) {
216
+ writeProjects(data);
217
+ }
218
+
219
+ return removed;
220
+ }
221
+
222
+ /**
223
+ * Get project statistics
224
+ * @returns {Object} Statistics object
225
+ */
226
+ function getProjectStats() {
227
+ const data = readProjects();
228
+
229
+ // Count by type
230
+ const byType = {};
231
+ data.projects.forEach(p => {
232
+ byType[p.type] = (byType[p.type] || 0) + 1;
233
+ });
234
+
235
+ // Most used IDE
236
+ const ideCount = {};
237
+ data.projects.forEach(p => {
238
+ if (p.ide) {
239
+ ideCount[p.ide] = (ideCount[p.ide] || 0) + 1;
240
+ }
241
+ });
242
+
243
+ const mostUsedIDE = Object.entries(ideCount)
244
+ .sort((a, b) => b[1] - a[1])[0]?.[0] || 'None';
245
+
246
+ return {
247
+ total: data.projects.length,
248
+ bookmarked: data.projects.filter(p => p.bookmarked).length,
249
+ byType,
250
+ mostUsedIDE,
251
+ oldest: data.projects.sort((a, b) =>
252
+ new Date(a.createdAt) - new Date(b.createdAt)
253
+ )[0],
254
+ newest: data.projects.sort((a, b) =>
255
+ new Date(b.createdAt) - new Date(a.createdAt)
256
+ )[0]
257
+ };
258
+ }
259
+
260
+ module.exports = {
261
+ addProject,
262
+ getAllProjects,
263
+ getProjectByName,
264
+ getProjectById,
265
+ getProjectByPath,
266
+ updateProject,
267
+ touchProject,
268
+ deleteProject,
269
+ getRecentProjects,
270
+ searchProjects,
271
+ cleanProjects,
272
+ getProjectStats
273
+ };
@@ -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
+ };