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
package/index.js
CHANGED
|
@@ -1,20 +1,76 @@
|
|
|
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.2.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 { recentCommand } = require('./src/commands/recent');
|
|
16
|
+
const { openCommand } = require('./src/commands/open');
|
|
17
|
+
const logger = require('./src/utils/logger');
|
|
15
18
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Package info
|
|
20
|
+
const packageJson = require('./package.json');
|
|
21
|
+
|
|
22
|
+
// Configure CLI
|
|
23
|
+
program
|
|
24
|
+
.name('pxo')
|
|
25
|
+
.description('Quick project setup and management for modern web frameworks')
|
|
26
|
+
.version(packageJson.version);
|
|
27
|
+
|
|
28
|
+
// Default command (no arguments) - Create new project
|
|
29
|
+
program
|
|
30
|
+
.action(() => {
|
|
31
|
+
createProject();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// List all projects
|
|
35
|
+
program
|
|
36
|
+
.command('list')
|
|
37
|
+
.alias('ls')
|
|
38
|
+
.description('List all tracked projects')
|
|
39
|
+
.action(() => {
|
|
40
|
+
listCommand();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Show recent projects
|
|
44
|
+
program
|
|
45
|
+
.command('recent')
|
|
46
|
+
.description('Show recently accessed projects')
|
|
47
|
+
.argument('[limit]', 'number of projects to show', '10')
|
|
48
|
+
.action((limit) => {
|
|
49
|
+
recentCommand(parseInt(limit));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Open project
|
|
53
|
+
program
|
|
54
|
+
.command('open [project-name]')
|
|
55
|
+
.description('Open a project in IDE')
|
|
56
|
+
.action((projectName) => {
|
|
57
|
+
openCommand(projectName);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Handle errors
|
|
61
|
+
program.exitOverride();
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
program.parse(process.argv);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error.code === 'commander.help') {
|
|
67
|
+
// Help was displayed, exit normally
|
|
68
|
+
process.exit(0);
|
|
69
|
+
} else if (error.code === 'commander.version') {
|
|
70
|
+
// Version was displayed, exit normally
|
|
71
|
+
process.exit(0);
|
|
72
|
+
} else {
|
|
73
|
+
// logger.error(`Error: ${error.message}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projxo",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.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,252 @@
|
|
|
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).padEnd(16);
|
|
71
|
+
const timeAgo = formatRelativeTime(project.lastAccessed).padEnd(15);
|
|
72
|
+
const nameDisplay = project.name.padEnd(30);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
name: `${nameDisplay} ${typeDisplay} ${timeAgo}`,
|
|
76
|
+
value: project.id,
|
|
77
|
+
short: project.name
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Add separator and action options
|
|
82
|
+
choices.push(
|
|
83
|
+
new inquirer.Separator(),
|
|
84
|
+
{ name: '← Back', value: 'back' }
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const { selectedId } = await inquirer.prompt([
|
|
88
|
+
{
|
|
89
|
+
type: 'list',
|
|
90
|
+
name: 'selectedId',
|
|
91
|
+
message: 'Select a project:',
|
|
92
|
+
choices,
|
|
93
|
+
pageSize: 15
|
|
94
|
+
}
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
if (selectedId === 'back') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Show actions for selected project
|
|
102
|
+
await showProjectActions(selectedId);
|
|
103
|
+
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error.isTtyError) {
|
|
106
|
+
logger.error('This command requires an interactive terminal');
|
|
107
|
+
} else {
|
|
108
|
+
logger.error(`Failed to list projects: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Show actions for a selected project
|
|
115
|
+
* @param {string} projectId - Project ID
|
|
116
|
+
*/
|
|
117
|
+
async function showProjectActions(projectId) {
|
|
118
|
+
const { getProjectById } = require('../storage/projects');
|
|
119
|
+
const project = getProjectById(projectId);
|
|
120
|
+
|
|
121
|
+
if (!project) {
|
|
122
|
+
logger.error('Project not found');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { action } = await inquirer.prompt([
|
|
127
|
+
{
|
|
128
|
+
type: 'list',
|
|
129
|
+
name: 'action',
|
|
130
|
+
message: `Actions for "${project.name}":`,
|
|
131
|
+
choices: [
|
|
132
|
+
{ name: '📂 Open in IDE', value: 'open' },
|
|
133
|
+
{ name: '📋 Copy path', value: 'copy' },
|
|
134
|
+
{ name: '🗑️ Remove from tracking', value: 'delete' },
|
|
135
|
+
{ name: 'ℹ️ Show details', value: 'details' },
|
|
136
|
+
new inquirer.Separator(),
|
|
137
|
+
{ name: '← Back to list', value: 'back' }
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
switch (action) {
|
|
143
|
+
case 'open':
|
|
144
|
+
await handleOpenProject(project);
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
case 'copy':
|
|
148
|
+
handleCopyPath(project);
|
|
149
|
+
break;
|
|
150
|
+
|
|
151
|
+
case 'delete':
|
|
152
|
+
await handleDeleteProject(project);
|
|
153
|
+
await listCommand(); // Refresh list
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case 'details':
|
|
157
|
+
showProjectDetails(project);
|
|
158
|
+
await showProjectActions(projectId); // Show actions again
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
case 'back':
|
|
162
|
+
await listCommand(); // Go back to list
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Handle opening project in IDE
|
|
169
|
+
* @param {Object} project - Project object
|
|
170
|
+
*/
|
|
171
|
+
async function handleOpenProject(project) {
|
|
172
|
+
// Update last accessed time
|
|
173
|
+
touchProject(project.id);
|
|
174
|
+
|
|
175
|
+
// Use project's preferred IDE or prompt
|
|
176
|
+
let ideKey = project.ide;
|
|
177
|
+
|
|
178
|
+
if (!ideKey || ideKey === 'skip') {
|
|
179
|
+
const { getIDEChoices } = require('../config/ides');
|
|
180
|
+
const { selectedIDE } = await inquirer.prompt([
|
|
181
|
+
{
|
|
182
|
+
type: 'list',
|
|
183
|
+
name: 'selectedIDE',
|
|
184
|
+
message: 'Select IDE:',
|
|
185
|
+
choices: getIDEChoices()
|
|
186
|
+
}
|
|
187
|
+
]);
|
|
188
|
+
ideKey = selectedIDE;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (ideKey !== 'skip') {
|
|
192
|
+
const success = await openInIDE(project.path, ideKey);
|
|
193
|
+
if (success) {
|
|
194
|
+
logger.success(`Opened ${project.name}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Handle copying project path
|
|
201
|
+
* @param {Object} project - Project object
|
|
202
|
+
*/
|
|
203
|
+
function handleCopyPath(project) {
|
|
204
|
+
// For now, just display the path
|
|
205
|
+
// In future, could use clipboard library
|
|
206
|
+
logger.info('Project path:');
|
|
207
|
+
logger.log(` ${project.path}`, 'cyan');
|
|
208
|
+
logger.log('\n(Copy from above)', 'dim');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Handle deleting project from tracking
|
|
213
|
+
* @param {Object} project - Project object
|
|
214
|
+
*/
|
|
215
|
+
async function handleDeleteProject(project) {
|
|
216
|
+
const { confirm } = await inquirer.prompt([
|
|
217
|
+
{
|
|
218
|
+
type: 'confirm',
|
|
219
|
+
name: 'confirm',
|
|
220
|
+
message: `Remove "${project.name}" from tracking? (Files won't be deleted)`,
|
|
221
|
+
default: false
|
|
222
|
+
}
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
if (confirm) {
|
|
226
|
+
deleteProject(project.id);
|
|
227
|
+
logger.success(`Removed ${project.name} from tracking`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Show detailed project information
|
|
233
|
+
* @param {Object} project - Project object
|
|
234
|
+
*/
|
|
235
|
+
function showProjectDetails(project) {
|
|
236
|
+
logger.newLine();
|
|
237
|
+
logger.log('━'.repeat(50), 'dim');
|
|
238
|
+
logger.log(` ${project.name}`, 'bright');
|
|
239
|
+
logger.log('━'.repeat(50), 'dim');
|
|
240
|
+
logger.log(` Type: ${getTypeDisplay(project.type)}`, 'cyan');
|
|
241
|
+
logger.log(` Path: ${project.path}`, 'dim');
|
|
242
|
+
logger.log(` Created: ${new Date(project.createdAt).toLocaleString()}`, 'dim');
|
|
243
|
+
logger.log(` Last accessed: ${formatRelativeTime(project.lastAccessed)}`, 'dim');
|
|
244
|
+
if (project.ide) {
|
|
245
|
+
const ide = getIDE(project.ide);
|
|
246
|
+
logger.log(` Default IDE: ${ide?.name || project.ide}`, 'dim');
|
|
247
|
+
}
|
|
248
|
+
logger.log('━'.repeat(50), 'dim');
|
|
249
|
+
logger.newLine();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = { listCommand };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open command - Quick open project by name
|
|
3
|
+
* Usage: pxo open <project-name>
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
const { getProjectByName, touchProject, searchProjects } = require('../storage/projects');
|
|
8
|
+
const { openInIDE } = require('../handlers/ideOpener');
|
|
9
|
+
const { getIDEChoices } = require('../config/ides');
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Execute open command
|
|
14
|
+
* @param {string} projectName - Project name to open
|
|
15
|
+
*/
|
|
16
|
+
async function openCommand(projectName) {
|
|
17
|
+
try {
|
|
18
|
+
if (!projectName) {
|
|
19
|
+
logger.error('Please provide a project name');
|
|
20
|
+
logger.log('\nUsage:', 'dim');
|
|
21
|
+
logger.log(' pxo open <project-name>', 'cyan');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Try to find project
|
|
26
|
+
let project = getProjectByName(projectName);
|
|
27
|
+
|
|
28
|
+
// If not found, try fuzzy search
|
|
29
|
+
if (!project) {
|
|
30
|
+
const matches = searchProjects(projectName);
|
|
31
|
+
|
|
32
|
+
if (matches.length === 0) {
|
|
33
|
+
logger.error(`Project "${projectName}" not found`);
|
|
34
|
+
logger.log('\nList all projects with:', 'dim');
|
|
35
|
+
logger.log(' pxo list', 'cyan');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (matches.length === 1) {
|
|
40
|
+
project = matches[0];
|
|
41
|
+
logger.info(`Found similar project: ${project.name}`);
|
|
42
|
+
} else {
|
|
43
|
+
// Multiple matches, let user choose
|
|
44
|
+
project = await selectFromMatches(matches);
|
|
45
|
+
if (!project) return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Update last accessed time
|
|
50
|
+
touchProject(project.id);
|
|
51
|
+
|
|
52
|
+
// Determine which IDE to use
|
|
53
|
+
let ideKey = project.ide;
|
|
54
|
+
|
|
55
|
+
if (!ideKey || ideKey === 'skip') {
|
|
56
|
+
const { selectedIDE } = await inquirer.prompt([
|
|
57
|
+
{
|
|
58
|
+
type: 'list',
|
|
59
|
+
name: 'selectedIDE',
|
|
60
|
+
message: `Open "${project.name}" in:`,
|
|
61
|
+
choices: getIDEChoices()
|
|
62
|
+
}
|
|
63
|
+
]);
|
|
64
|
+
ideKey = selectedIDE;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Open in IDE
|
|
68
|
+
if (ideKey !== 'skip') {
|
|
69
|
+
const success = await openInIDE(project.path, ideKey);
|
|
70
|
+
if (success) {
|
|
71
|
+
logger.success(`Opened ${project.name}`);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
logger.info(`Project path: ${project.path}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.error(`Failed to open project: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Let user select from multiple matches
|
|
84
|
+
* @param {Array} matches - Array of matching projects
|
|
85
|
+
* @returns {Promise<Object|null>} Selected project or null
|
|
86
|
+
*/
|
|
87
|
+
async function selectFromMatches(matches) {
|
|
88
|
+
logger.info(`Found ${matches.length} matching projects:`);
|
|
89
|
+
logger.newLine();
|
|
90
|
+
|
|
91
|
+
const choices = matches.map(p => ({
|
|
92
|
+
name: `${p.name} (${p.type})`,
|
|
93
|
+
value: p.id,
|
|
94
|
+
short: p.name
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
choices.push(
|
|
98
|
+
new inquirer.Separator(),
|
|
99
|
+
{ name: '← Cancel', value: 'cancel' }
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const { selectedId } = await inquirer.prompt([
|
|
103
|
+
{
|
|
104
|
+
type: 'list',
|
|
105
|
+
name: 'selectedId',
|
|
106
|
+
message: 'Which project did you mean?',
|
|
107
|
+
choices
|
|
108
|
+
}
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
if (selectedId === 'cancel') {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { getProjectById } = require('../storage/projects');
|
|
116
|
+
return getProjectById(selectedId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { openCommand };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recent command - Show recently accessed projects
|
|
3
|
+
* Usage: pxo recent [limit]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
const { getRecentProjects, touchProject } = require('../storage/projects');
|
|
8
|
+
const { openInIDE } = require('../handlers/ideOpener');
|
|
9
|
+
const logger = require('../utils/logger');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format relative time (same as list.js)
|
|
13
|
+
* @param {string} isoDate - ISO date string
|
|
14
|
+
* @returns {string} Human-readable relative time
|
|
15
|
+
*/
|
|
16
|
+
function formatRelativeTime(isoDate) {
|
|
17
|
+
const date = new Date(isoDate);
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const diffMs = now - date;
|
|
20
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
21
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
22
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
23
|
+
|
|
24
|
+
if (diffMins < 1) return 'just now';
|
|
25
|
+
if (diffMins < 60) return `${diffMins} min${diffMins > 1 ? 's' : ''} ago`;
|
|
26
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
|
27
|
+
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
|
28
|
+
if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`;
|
|
29
|
+
return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get project type display name
|
|
34
|
+
* @param {string} type - Project type key
|
|
35
|
+
* @returns {string} Formatted display name
|
|
36
|
+
*/
|
|
37
|
+
function getTypeDisplay(type) {
|
|
38
|
+
const typeMap = {
|
|
39
|
+
'react-vite': 'React+Vite',
|
|
40
|
+
'react-vite-ts': 'React+Vite(TS)',
|
|
41
|
+
'nextjs': 'Next.js',
|
|
42
|
+
'angular': 'Angular',
|
|
43
|
+
'react-native': 'React Native'
|
|
44
|
+
};
|
|
45
|
+
return typeMap[type] || type;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Execute recent command
|
|
50
|
+
* @param {number} limit - Number of recent projects to show
|
|
51
|
+
*/
|
|
52
|
+
async function recentCommand(limit = 10) {
|
|
53
|
+
try {
|
|
54
|
+
const projects = getRecentProjects(limit);
|
|
55
|
+
|
|
56
|
+
if (projects.length === 0) {
|
|
57
|
+
logger.info('No recent projects found');
|
|
58
|
+
logger.log('\nCreate your first project with:', 'dim');
|
|
59
|
+
logger.log(' pxo', 'cyan');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.newLine();
|
|
64
|
+
logger.log(`🕐 Recent Projects (${projects.length})`, 'bright');
|
|
65
|
+
logger.newLine();
|
|
66
|
+
|
|
67
|
+
// Create choices for inquirer
|
|
68
|
+
const choices = projects.map((project, index) => {
|
|
69
|
+
const typeDisplay = getTypeDisplay(project.type).padEnd(16);
|
|
70
|
+
const timeAgo = formatRelativeTime(project.lastAccessed).padEnd(15);
|
|
71
|
+
const indexDisplay = `${index + 1}.`.padEnd(4);
|
|
72
|
+
const nameDisplay = project.name.padEnd(30);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
name: `${indexDisplay}${nameDisplay} ${typeDisplay} ${timeAgo}`,
|
|
76
|
+
value: project.id,
|
|
77
|
+
short: project.name
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Add separator and back option
|
|
82
|
+
choices.push(
|
|
83
|
+
new inquirer.Separator(),
|
|
84
|
+
{ name: '← Cancel', value: 'cancel' }
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const { selectedId } = await inquirer.prompt([
|
|
88
|
+
{
|
|
89
|
+
type: 'list',
|
|
90
|
+
name: 'selectedId',
|
|
91
|
+
message: 'Select a project to open:',
|
|
92
|
+
choices,
|
|
93
|
+
pageSize: 15
|
|
94
|
+
}
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
if (selectedId === 'cancel') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Open selected project
|
|
102
|
+
await openSelectedProject(selectedId);
|
|
103
|
+
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error.isTtyError) {
|
|
106
|
+
logger.error('This command requires an interactive terminal');
|
|
107
|
+
} else {
|
|
108
|
+
logger.error(`Failed to show recent projects: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Open selected project in IDE
|
|
115
|
+
* @param {string} projectId - Project ID
|
|
116
|
+
*/
|
|
117
|
+
async function openSelectedProject(projectId) {
|
|
118
|
+
const { getProjectById } = require('../storage/projects');
|
|
119
|
+
const { getIDEChoices } = require('../config/ides');
|
|
120
|
+
|
|
121
|
+
const project = getProjectById(projectId);
|
|
122
|
+
|
|
123
|
+
if (!project) {
|
|
124
|
+
logger.error('Project not found');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Update last accessed time
|
|
129
|
+
touchProject(project.id);
|
|
130
|
+
|
|
131
|
+
// Use project's preferred IDE or prompt
|
|
132
|
+
let ideKey = project.ide;
|
|
133
|
+
|
|
134
|
+
if (!ideKey || ideKey === 'skip') {
|
|
135
|
+
const { selectedIDE } = await inquirer.prompt([
|
|
136
|
+
{
|
|
137
|
+
type: 'list',
|
|
138
|
+
name: 'selectedIDE',
|
|
139
|
+
message: 'Select IDE:',
|
|
140
|
+
choices: getIDEChoices()
|
|
141
|
+
}
|
|
142
|
+
]);
|
|
143
|
+
ideKey = selectedIDE;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (ideKey !== 'skip') {
|
|
147
|
+
const success = await openInIDE(project.path, ideKey);
|
|
148
|
+
if (success) {
|
|
149
|
+
logger.success(`Opened ${project.name}`);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
logger.info(`Project path: ${project.path}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { recentCommand };
|
|
@@ -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) {
|