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
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
|
-
*
|
|
7
|
+
* Handles command routing and argument parsing
|
|
8
8
|
*
|
|
9
|
-
* @
|
|
10
|
-
* @version 1.0.1
|
|
9
|
+
* @version 1.1.0
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
|
-
|
|
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
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
+
}
|