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/Readme.md +290 -371
- package/index.js +67 -11
- package/package.json +3 -2
- package/src/cli.js +11 -0
- package/src/commands/list.js +252 -0
- package/src/commands/open.js +119 -0
- package/src/commands/recent.js +156 -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,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
|
+
};
|