projxo 1.0.1 ā 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 +4 -2
- package/src/cli.js +184 -0
- package/src/commands/list.js +251 -0
- package/src/config/ides.js +129 -0
- package/src/config/projectTypes.js +145 -0
- package/src/handlers/ideOpener.js +73 -0
- package/src/handlers/projectCreator.js +151 -0
- package/src/prompts/questions.js +105 -0
- package/src/storage/database.js +149 -0
- package/src/storage/debug-storage.js +99 -0
- package/src/storage/projects.js +273 -0
- package/src/utils/command.js +64 -0
- package/src/utils/fileSystem.js +147 -0
- package/src/utils/logger.js +110 -0
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command execution utility
|
|
3
|
+
* Handles spawning child processes and command execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const logger = require('./logger');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Execute a shell command
|
|
11
|
+
* @param {string} command - Command to execute (e.g., 'npm', 'npx')
|
|
12
|
+
* @param {string[]} args - Array of command arguments
|
|
13
|
+
* @param {string} [cwd] - Working directory for command execution
|
|
14
|
+
* @returns {Promise<void>} Resolves when command completes successfully
|
|
15
|
+
* @throws {Error} If command exits with non-zero code
|
|
16
|
+
*/
|
|
17
|
+
function runCommand(command, args, cwd) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
// Log the command being executed for debugging
|
|
20
|
+
logger.log(`\nExecuting: ${command} ${args.join(' ')}`, 'cyan');
|
|
21
|
+
|
|
22
|
+
// Spawn the process
|
|
23
|
+
const proc = spawn(command, args, {
|
|
24
|
+
stdio: 'inherit', // Pipe stdin, stdout, stderr to parent process
|
|
25
|
+
shell: true, // Use shell for command execution
|
|
26
|
+
cwd: cwd || process.cwd() // Use provided cwd or current directory
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Handle process completion
|
|
30
|
+
proc.on('close', (code) => {
|
|
31
|
+
if (code !== 0) {
|
|
32
|
+
reject(new Error(`Command failed with exit code ${code}`));
|
|
33
|
+
} else {
|
|
34
|
+
resolve();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Handle process errors (e.g., command not found)
|
|
39
|
+
proc.on('error', (err) => {
|
|
40
|
+
reject(new Error(`Failed to execute command: ${err.message}`));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if a command exists in the system PATH
|
|
47
|
+
* @param {string} command - Command to check
|
|
48
|
+
* @returns {Promise<boolean>} True if command exists
|
|
49
|
+
*/
|
|
50
|
+
async function commandExists(command) {
|
|
51
|
+
const checkCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await runCommand(checkCmd, [command], process.cwd());
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
runCommand,
|
|
63
|
+
commandExists
|
|
64
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File system utility functions
|
|
3
|
+
* Handles all file and directory operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const logger = require('./logger');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Expand tilde (~) in path to home directory
|
|
13
|
+
* @param {string} filePath - Path that may contain ~
|
|
14
|
+
* @returns {string} Expanded absolute path
|
|
15
|
+
* @example
|
|
16
|
+
* expandHomePath('~/Documents') // -> '/Users/username/Documents'
|
|
17
|
+
*/
|
|
18
|
+
function expandHomePath(filePath) {
|
|
19
|
+
if (filePath.startsWith('~')) {
|
|
20
|
+
return filePath.replace('~', os.homedir());
|
|
21
|
+
}
|
|
22
|
+
return filePath;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ensure a directory exists, create if it doesn't
|
|
27
|
+
* @param {string} dirPath - Directory path to ensure
|
|
28
|
+
* @returns {boolean} True if directory was created, false if already existed
|
|
29
|
+
*/
|
|
30
|
+
function ensureDirectory(dirPath) {
|
|
31
|
+
const expandedPath = expandHomePath(dirPath);
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(expandedPath)) {
|
|
34
|
+
fs.mkdirSync(expandedPath, { recursive: true });
|
|
35
|
+
logger.success(`Created directory: ${expandedPath}`);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a path exists
|
|
44
|
+
* @param {string} filePath - Path to check
|
|
45
|
+
* @returns {boolean} True if path exists
|
|
46
|
+
*/
|
|
47
|
+
function pathExists(filePath) {
|
|
48
|
+
return fs.existsSync(expandHomePath(filePath));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a path is a directory
|
|
53
|
+
* @param {string} dirPath - Path to check
|
|
54
|
+
* @returns {boolean} True if path exists and is a directory
|
|
55
|
+
*/
|
|
56
|
+
function isDirectory(dirPath) {
|
|
57
|
+
try {
|
|
58
|
+
const stats = fs.statSync(expandHomePath(dirPath));
|
|
59
|
+
return stats.isDirectory();
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the full absolute path
|
|
67
|
+
* @param {string} dirPath - Directory path
|
|
68
|
+
* @param {string} projectName - Project name
|
|
69
|
+
* @returns {string} Full project path
|
|
70
|
+
*/
|
|
71
|
+
function getProjectPath(dirPath, projectName) {
|
|
72
|
+
const expandedDir = expandHomePath(dirPath);
|
|
73
|
+
return path.join(expandedDir, projectName);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate directory path
|
|
78
|
+
* @param {string} dirPath - Directory path to validate
|
|
79
|
+
* @returns {Object} { valid: boolean, error?: string }
|
|
80
|
+
*/
|
|
81
|
+
function validateDirectoryPath(dirPath) {
|
|
82
|
+
const expandedPath = expandHomePath(dirPath);
|
|
83
|
+
|
|
84
|
+
// Check if path contains invalid characters
|
|
85
|
+
const invalidChars = /[<>:"|?*]/;
|
|
86
|
+
if (invalidChars.test(expandedPath)) {
|
|
87
|
+
return {
|
|
88
|
+
valid: false,
|
|
89
|
+
error: 'Path contains invalid characters'
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if parent directory exists (if path doesn't exist)
|
|
94
|
+
if (!pathExists(expandedPath)) {
|
|
95
|
+
const parentDir = path.dirname(expandedPath);
|
|
96
|
+
if (!pathExists(parentDir)) {
|
|
97
|
+
return {
|
|
98
|
+
valid: false,
|
|
99
|
+
error: 'Parent directory does not exist'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { valid: true };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Validate project name
|
|
109
|
+
* @param {string} projectName - Project name to validate
|
|
110
|
+
* @returns {Object} { valid: boolean, error?: string }
|
|
111
|
+
*/
|
|
112
|
+
function validateProjectName(projectName) {
|
|
113
|
+
if (!projectName || !projectName.trim()) {
|
|
114
|
+
return {
|
|
115
|
+
valid: false,
|
|
116
|
+
error: 'Project name cannot be empty'
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Allow letters, numbers, hyphens, underscores, dots
|
|
121
|
+
if (!/^[a-zA-Z0-9-_.]+$/.test(projectName)) {
|
|
122
|
+
return {
|
|
123
|
+
valid: false,
|
|
124
|
+
error: 'Project name can only contain letters, numbers, hyphens, underscores, and dots'
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Don't allow names starting with dots or hyphens
|
|
129
|
+
if (/^[.-]/.test(projectName)) {
|
|
130
|
+
return {
|
|
131
|
+
valid: false,
|
|
132
|
+
error: 'Project name cannot start with a dot or hyphen'
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { valid: true };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
expandHomePath,
|
|
141
|
+
ensureDirectory,
|
|
142
|
+
pathExists,
|
|
143
|
+
isDirectory,
|
|
144
|
+
getProjectPath,
|
|
145
|
+
validateDirectoryPath,
|
|
146
|
+
validateProjectName
|
|
147
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utility for colored terminal output
|
|
3
|
+
* Provides consistent formatting across the application
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ANSI color codes for terminal styling
|
|
7
|
+
const COLORS = {
|
|
8
|
+
reset: '\x1b[0m',
|
|
9
|
+
bright: '\x1b[1m',
|
|
10
|
+
dim: '\x1b[2m',
|
|
11
|
+
|
|
12
|
+
// Standard colors
|
|
13
|
+
black: '\x1b[30m',
|
|
14
|
+
red: '\x1b[31m',
|
|
15
|
+
green: '\x1b[32m',
|
|
16
|
+
yellow: '\x1b[33m',
|
|
17
|
+
blue: '\x1b[34m',
|
|
18
|
+
magenta: '\x1b[35m',
|
|
19
|
+
cyan: '\x1b[36m',
|
|
20
|
+
white: '\x1b[37m',
|
|
21
|
+
|
|
22
|
+
// Bright colors
|
|
23
|
+
brightRed: '\x1b[91m',
|
|
24
|
+
brightGreen: '\x1b[92m',
|
|
25
|
+
brightYellow: '\x1b[93m',
|
|
26
|
+
brightBlue: '\x1b[94m',
|
|
27
|
+
brightCyan: '\x1b[96m'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Main logging function
|
|
32
|
+
* @param {string} message - Message to log
|
|
33
|
+
* @param {string} color - Color key from COLORS object
|
|
34
|
+
*/
|
|
35
|
+
function log(message, color = 'reset') {
|
|
36
|
+
const colorCode = COLORS[color] || COLORS.reset;
|
|
37
|
+
console.log(`${colorCode}${message}${COLORS.reset}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Log success message with green checkmark
|
|
42
|
+
* @param {string} message - Success message
|
|
43
|
+
*/
|
|
44
|
+
function success(message) {
|
|
45
|
+
log(`ā ${message}`, 'green');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Log error message with red X
|
|
50
|
+
* @param {string} message - Error message
|
|
51
|
+
*/
|
|
52
|
+
function error(message) {
|
|
53
|
+
log(`ā ${message}`, 'red');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Log warning message with yellow exclamation
|
|
58
|
+
* @param {string} message - Warning message
|
|
59
|
+
*/
|
|
60
|
+
function warning(message) {
|
|
61
|
+
log(`ā ${message}`, 'yellow');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Log info message with blue icon
|
|
66
|
+
* @param {string} message - Info message
|
|
67
|
+
*/
|
|
68
|
+
function info(message) {
|
|
69
|
+
log(`ā¹ ${message}`, 'blue');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Log a horizontal separator line
|
|
74
|
+
* @param {number} length - Length of separator (default: 50)
|
|
75
|
+
* @param {string} color - Color of separator
|
|
76
|
+
*/
|
|
77
|
+
function separator(length = 50, color = 'bright') {
|
|
78
|
+
log('='.repeat(length), color);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Log a section header with separators
|
|
83
|
+
* @param {string} title - Section title
|
|
84
|
+
*/
|
|
85
|
+
function section(title) {
|
|
86
|
+
console.log(); // Empty line
|
|
87
|
+
separator();
|
|
88
|
+
log(title, 'bright');
|
|
89
|
+
separator();
|
|
90
|
+
console.log(); // Empty line
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Log an empty line
|
|
95
|
+
*/
|
|
96
|
+
function newLine() {
|
|
97
|
+
console.log();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
log,
|
|
102
|
+
success,
|
|
103
|
+
error,
|
|
104
|
+
warning,
|
|
105
|
+
info,
|
|
106
|
+
separator,
|
|
107
|
+
section,
|
|
108
|
+
newLine,
|
|
109
|
+
COLORS
|
|
110
|
+
};
|