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.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Project type configurations
3
+ * Defines all supported project types and their creation commands
4
+ *
5
+ * To add a new project type:
6
+ * 1. Add a new key to PROJECT_TYPES object
7
+ * 2. Specify name, command, getArgs function, and postInstall flag
8
+ * 3. Add corresponding help text to getNextSteps function
9
+ */
10
+
11
+ /**
12
+ * Project type configuration object
13
+ * @typedef {Object} ProjectTypeConfig
14
+ * @property {string} name - Display name for the project type
15
+ * @property {string} command - Command to execute (npm, npx, etc.)
16
+ * @property {Function} getArgs - Function that returns command arguments
17
+ * @property {boolean} postInstall - Whether to run npm install after creation
18
+ * @property {string} [description] - Optional description
19
+ */
20
+
21
+ const PROJECT_TYPES = {
22
+ 'react-vite': {
23
+ name: 'React + Vite',
24
+ description: 'React with Vite bundler (JavaScript)',
25
+ command: 'npm',
26
+ getArgs: (name) => ['create', 'vite@latest', name, '--', '--template', 'react'],
27
+ postInstall: true
28
+ },
29
+
30
+ 'react-vite-ts': {
31
+ name: 'React + Vite (TypeScript)',
32
+ description: 'React with Vite bundler and TypeScript',
33
+ command: 'npm',
34
+ getArgs: (name) => ['create', 'vite@latest', name, '--', '--template', 'react-ts'],
35
+ postInstall: true
36
+ },
37
+
38
+ 'nextjs': {
39
+ name: 'Next.js',
40
+ description: 'React framework for production',
41
+ command: 'npx',
42
+ getArgs: (name) => ['create-next-app@latest', name],
43
+ postInstall: false // create-next-app already installs dependencies
44
+ },
45
+
46
+ 'angular': {
47
+ name: 'Angular',
48
+ description: 'Platform for building web applications',
49
+ command: 'npx',
50
+ getArgs: (name) => ['@angular/cli@latest', 'new', name],
51
+ postInstall: false // Angular CLI already installs dependencies
52
+ },
53
+
54
+ 'react-native': {
55
+ name: 'React Native (Expo)',
56
+ description: 'Build native mobile apps with React',
57
+ command: 'npx',
58
+ getArgs: (name) => ['create-expo-app', name],
59
+ postInstall: false // Expo already installs dependencies
60
+ }
61
+ };
62
+
63
+ /**
64
+ * Get project type configuration
65
+ * @param {string} typeKey - Project type key
66
+ * @returns {ProjectTypeConfig|null} Configuration object or null if not found
67
+ */
68
+ function getProjectType(typeKey) {
69
+ return PROJECT_TYPES[typeKey] || null;
70
+ }
71
+
72
+ /**
73
+ * Get all project types as an array
74
+ * @returns {Array<{key: string, config: ProjectTypeConfig}>}
75
+ */
76
+ function getAllProjectTypes() {
77
+ return Object.entries(PROJECT_TYPES).map(([key, config]) => ({
78
+ key,
79
+ config
80
+ }));
81
+ }
82
+
83
+ /**
84
+ * Get formatted choices for inquirer prompts
85
+ * @returns {Array<{name: string, value: string}>}
86
+ */
87
+ function getProjectTypeChoices() {
88
+ return Object.entries(PROJECT_TYPES).map(([key, config]) => ({
89
+ name: `${config.name} (${key})`,
90
+ value: key,
91
+ short: config.name
92
+ }));
93
+ }
94
+
95
+ /**
96
+ * Get next steps instructions based on project type
97
+ * @param {string} typeKey - Project type key
98
+ * @param {string} projectPath - Full path to the project
99
+ * @returns {string[]} Array of instruction strings
100
+ */
101
+ function getNextSteps(typeKey, projectPath) {
102
+ const steps = [`cd ${projectPath}`];
103
+
104
+ switch (typeKey) {
105
+ case 'react-vite':
106
+ case 'react-vite-ts':
107
+ steps.push('npm run dev');
108
+ break;
109
+
110
+ case 'nextjs':
111
+ steps.push('npm run dev');
112
+ break;
113
+
114
+ case 'angular':
115
+ steps.push('ng serve');
116
+ break;
117
+
118
+ case 'react-native':
119
+ steps.push('npx expo start');
120
+ break;
121
+
122
+ default:
123
+ steps.push('npm start');
124
+ }
125
+
126
+ return steps;
127
+ }
128
+
129
+ /**
130
+ * Check if a project type exists
131
+ * @param {string} typeKey - Project type key to check
132
+ * @returns {boolean}
133
+ */
134
+ function isValidProjectType(typeKey) {
135
+ return typeKey in PROJECT_TYPES;
136
+ }
137
+
138
+ module.exports = {
139
+ PROJECT_TYPES,
140
+ getProjectType,
141
+ getAllProjectTypes,
142
+ getProjectTypeChoices,
143
+ getNextSteps,
144
+ isValidProjectType
145
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * IDE opener handler
3
+ * Handles opening projects in various IDEs
4
+ */
5
+
6
+ const { runCommand } = require('../utils/command');
7
+ const { getIDE, getIDEInstallInstructions } = require('../config/ides');
8
+ const logger = require('../utils/logger');
9
+
10
+ /**
11
+ * Open a project in the specified IDE
12
+ * @param {string} projectPath - Full path to the project
13
+ * @param {string} ideKey - IDE key (e.g., 'vscode', 'cursor')
14
+ * @returns {Promise<boolean>} True if opened successfully, false otherwise
15
+ */
16
+ async function openInIDE(projectPath, ideKey) {
17
+ // Get IDE configuration
18
+ const ide = getIDE(ideKey);
19
+
20
+ // Skip if no IDE selected or invalid IDE
21
+ if (!ide || !ide.command) {
22
+ return false;
23
+ }
24
+
25
+ try {
26
+ logger.info(`Opening project in ${ide.name}...`);
27
+
28
+ // Execute IDE command with project path
29
+ await runCommand(ide.command, [projectPath]);
30
+
31
+ logger.success(`Project opened in ${ide.name}`);
32
+ return true;
33
+
34
+ } catch (error) {
35
+ // Handle IDE command failure
36
+ logger.error(`Could not open ${ide.name}`);
37
+ logger.warning(`Please ensure ${ide.name} is installed and command-line tools are enabled`);
38
+
39
+ // Provide installation instructions
40
+ const instructions = getIDEInstallInstructions(ideKey);
41
+ logger.log(`\n Setup: ${instructions}`, 'dim');
42
+
43
+ // Provide fallback option
44
+ logger.log(` Or manually open: ${projectPath}`, 'yellow');
45
+
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Validate if IDE command is available
52
+ * @param {string} ideKey - IDE key to validate
53
+ * @returns {Promise<boolean>} True if command is available
54
+ */
55
+ async function validateIDECommand(ideKey) {
56
+ const ide = getIDE(ideKey);
57
+
58
+ if (!ide || !ide.command) {
59
+ return true; // Skip validation for 'skip' option
60
+ }
61
+
62
+ try {
63
+ const { commandExists } = require('../utils/command');
64
+ return await commandExists(ide.command);
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ module.exports = {
71
+ openInIDE,
72
+ validateIDECommand
73
+ };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Project creator handler
3
+ * Handles the project creation process
4
+ */
5
+
6
+ const { runCommand } = require('../utils/command');
7
+ const {
8
+ expandHomePath,
9
+ ensureDirectory,
10
+ pathExists,
11
+ getProjectPath
12
+ } = require('../utils/fileSystem');
13
+ const { getProjectType, getNextSteps } = require('../config/projectTypes');
14
+ const logger = require('../utils/logger');
15
+
16
+ /**
17
+ * Create a new project
18
+ * @param {Object} options - Project creation options
19
+ * @param {string} options.projectType - Project type key
20
+ * @param {string} options.projectName - Name of the project
21
+ * @param {string} options.directory - Directory path where project will be created
22
+ * @returns {Promise<string>} Full path to the created project
23
+ * @throws {Error} If project creation fails
24
+ */
25
+ async function createProject({ projectType, projectName, directory }) {
26
+ // Get project type configuration
27
+ const config = getProjectType(projectType);
28
+
29
+ if (!config) {
30
+ throw new Error(`Invalid project type: ${projectType}`);
31
+ }
32
+
33
+ // Expand home directory and ensure directory exists
34
+ const expandedDir = expandHomePath(directory);
35
+ ensureDirectory(expandedDir);
36
+
37
+ // Get full project path
38
+ const fullPath = getProjectPath(expandedDir, projectName);
39
+
40
+ // Check if project already exists
41
+ if (pathExists(fullPath)) {
42
+ throw new Error('PROJECT_EXISTS');
43
+ }
44
+
45
+ // Display creation start message
46
+ logger.section('Starting project creation...');
47
+
48
+ try {
49
+ // Execute project creation command in the target directory
50
+ await runCommand(
51
+ config.command,
52
+ config.getArgs(projectName),
53
+ expandedDir
54
+ );
55
+
56
+ // Run post-install if needed
57
+ if (config.postInstall) {
58
+ logger.info('Installing dependencies...');
59
+ await runCommand('npm', ['install'], fullPath);
60
+ }
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
+
80
+ return fullPath;
81
+
82
+ } catch (error) {
83
+ // Provide helpful error message
84
+ throw new Error(`Failed to create project: ${error.message}`);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Display project creation success message with next steps
90
+ * @param {string} projectPath - Full path to the created project
91
+ * @param {string} projectType - Project type key
92
+ */
93
+ function displaySuccessMessage(projectPath, projectType) {
94
+ logger.section('✓ Project created successfully!');
95
+
96
+ logger.log('Project location:', 'bright');
97
+ logger.log(` ${projectPath}`, 'cyan');
98
+
99
+ logger.newLine();
100
+ logger.log('Next steps:', 'bright');
101
+
102
+ const steps = getNextSteps(projectType, projectPath);
103
+ steps.forEach(step => {
104
+ logger.log(` ${step}`, 'green');
105
+ });
106
+
107
+ logger.newLine();
108
+ }
109
+
110
+ /**
111
+ * Validate project creation parameters
112
+ * @param {Object} params - Parameters to validate
113
+ * @param {string} params.projectType - Project type key
114
+ * @param {string} params.projectName - Project name
115
+ * @param {string} params.directory - Directory path
116
+ * @returns {Object} { valid: boolean, error?: string }
117
+ */
118
+ function validateProjectParams({ projectType, projectName, directory }) {
119
+ // Validate project type
120
+ const config = getProjectType(projectType);
121
+ if (!config) {
122
+ return {
123
+ valid: false,
124
+ error: `Invalid project type: ${projectType}`
125
+ };
126
+ }
127
+
128
+ // Validate project name
129
+ if (!projectName || projectName.trim() === '') {
130
+ return {
131
+ valid: false,
132
+ error: 'Project name is required'
133
+ };
134
+ }
135
+
136
+ // Validate directory
137
+ if (!directory || directory.trim() === '') {
138
+ return {
139
+ valid: false,
140
+ error: 'Directory is required'
141
+ };
142
+ }
143
+
144
+ return { valid: true };
145
+ }
146
+
147
+ module.exports = {
148
+ createProject,
149
+ displaySuccessMessage,
150
+ validateProjectParams
151
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Inquirer prompt definitions
3
+ * Centralizes all user prompts for consistency
4
+ */
5
+
6
+ const { getProjectTypeChoices } = require('../config/projectTypes');
7
+ const { getIDEChoices } = require('../config/ides');
8
+ const { validateProjectName } = require('../utils/fileSystem');
9
+
10
+ /**
11
+ * Get project type selection prompt
12
+ * @returns {Object} Inquirer prompt configuration
13
+ */
14
+ function getProjectTypePrompt() {
15
+ return {
16
+ type: 'list',
17
+ name: 'projectType',
18
+ message: 'Select project type:',
19
+ choices: getProjectTypeChoices(),
20
+ pageSize: 10
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Get project name input prompt
26
+ * @returns {Object} Inquirer prompt configuration
27
+ */
28
+ function getProjectNamePrompt() {
29
+ return {
30
+ type: 'input',
31
+ name: 'projectName',
32
+ message: 'Enter project name:',
33
+ validate: (input) => {
34
+ const result = validateProjectName(input);
35
+ return result.valid ? true : result.error;
36
+ },
37
+ filter: (input) => input.trim() // Remove leading/trailing whitespace
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Get directory path input prompt
43
+ * @param {string} defaultPath - Default directory path
44
+ * @returns {Object} Inquirer prompt configuration
45
+ */
46
+ function getDirectoryPrompt(defaultPath) {
47
+ return {
48
+ type: 'input',
49
+ name: 'directory',
50
+ message: 'Enter directory path:',
51
+ default: defaultPath,
52
+ filter: (input) => input.trim()
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Get IDE selection prompt
58
+ * @returns {Object} Inquirer prompt configuration
59
+ */
60
+ function getIDEPrompt() {
61
+ return {
62
+ type: 'list',
63
+ name: 'selectedIDE',
64
+ message: 'Select IDE to open:',
65
+ choices: getIDEChoices(),
66
+ pageSize: 10
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Get overwrite confirmation prompt
72
+ * @param {string} projectName - Name of the project that already exists
73
+ * @returns {Object} Inquirer prompt configuration
74
+ */
75
+ function getOverwritePrompt(projectName) {
76
+ return {
77
+ type: 'confirm',
78
+ name: 'overwrite',
79
+ message: `Project "${projectName}" already exists. Overwrite?`,
80
+ default: false
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Get all prompts for project creation flow
86
+ * @param {string} defaultDirectory - Default directory path
87
+ * @returns {Array<Object>} Array of prompt configurations
88
+ */
89
+ function getProjectCreationPrompts(defaultDirectory) {
90
+ return [
91
+ getProjectTypePrompt(),
92
+ getProjectNamePrompt(),
93
+ getDirectoryPrompt(defaultDirectory),
94
+ getIDEPrompt()
95
+ ];
96
+ }
97
+
98
+ module.exports = {
99
+ getProjectTypePrompt,
100
+ getProjectNamePrompt,
101
+ getDirectoryPrompt,
102
+ getIDEPrompt,
103
+ getOverwritePrompt,
104
+ getProjectCreationPrompts
105
+ };
@@ -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
+ };