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/Readme.md +187 -402
- package/index.js +48 -11
- package/package.json +3 -2
- package/src/cli.js +11 -0
- package/src/commands/list.js +251 -0
- package/src/handlers/projectCreator.js +18 -0
- package/src/storage/database.js +149 -0
- package/src/storage/debug-storage.js +99 -0
- package/src/storage/projects.js +273 -0
|
@@ -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
|
+
};
|