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.
@@ -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
+ }
@@ -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
+ };