projax 3.3.39 → 3.3.51

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.
Files changed (115) hide show
  1. package/README.md +73 -0
  2. package/dist/__tests__/port-scanner.test.js +7 -17
  3. package/dist/__tests__/script-runner.test.js +7 -17
  4. package/dist/api/__tests__/database.test.js +7 -17
  5. package/dist/api/__tests__/database.test.js.map +1 -1
  6. package/dist/api/__tests__/routes.test.js +7 -17
  7. package/dist/api/__tests__/routes.test.js.map +1 -1
  8. package/dist/api/__tests__/scanner.test.js +7 -17
  9. package/dist/api/__tests__/scanner.test.js.map +1 -1
  10. package/dist/api/database.d.ts +20 -1
  11. package/dist/api/database.d.ts.map +1 -1
  12. package/dist/api/database.js +192 -21
  13. package/dist/api/database.js.map +1 -1
  14. package/dist/api/index.d.ts.map +1 -1
  15. package/dist/api/index.js +17 -19
  16. package/dist/api/index.js.map +1 -1
  17. package/dist/api/migrate.js +2 -1
  18. package/dist/api/migrate.js.map +1 -1
  19. package/dist/api/package.json +3 -2
  20. package/dist/api/routes/backup.d.ts +3 -0
  21. package/dist/api/routes/backup.d.ts.map +1 -0
  22. package/dist/api/routes/backup.js +51 -0
  23. package/dist/api/routes/backup.js.map +1 -0
  24. package/dist/api/routes/index.d.ts.map +1 -1
  25. package/dist/api/routes/index.js +6 -0
  26. package/dist/api/routes/index.js.map +1 -1
  27. package/dist/api/routes/mcp.d.ts +3 -0
  28. package/dist/api/routes/mcp.d.ts.map +1 -0
  29. package/dist/api/routes/mcp.js +147 -0
  30. package/dist/api/routes/mcp.js.map +1 -0
  31. package/dist/api/routes/projects.d.ts.map +1 -1
  32. package/dist/api/routes/projects.js +27 -17
  33. package/dist/api/routes/projects.js.map +1 -1
  34. package/dist/api/routes/settings.d.ts.map +1 -1
  35. package/dist/api/routes/settings.js +119 -11
  36. package/dist/api/routes/settings.js.map +1 -1
  37. package/dist/api/routes/workspaces.d.ts +3 -0
  38. package/dist/api/routes/workspaces.d.ts.map +1 -0
  39. package/dist/api/routes/workspaces.js +504 -0
  40. package/dist/api/routes/workspaces.js.map +1 -0
  41. package/dist/api/services/scanner.js +10 -19
  42. package/dist/api/services/scanner.js.map +1 -1
  43. package/dist/api/services/test-parser.js +3 -2
  44. package/dist/api/services/test-parser.js.map +1 -1
  45. package/dist/api/types.d.ts +31 -0
  46. package/dist/api/types.d.ts.map +1 -1
  47. package/dist/core/__tests__/database.test.js +7 -17
  48. package/dist/core/__tests__/detector.test.js +7 -17
  49. package/dist/core/__tests__/index.test.js +7 -17
  50. package/dist/core/__tests__/scanner.test.js +7 -17
  51. package/dist/core/__tests__/settings.test.js +7 -17
  52. package/dist/core/backup-utils.d.ts +17 -0
  53. package/dist/core/backup-utils.js +157 -0
  54. package/dist/core/database.d.ts +1 -0
  55. package/dist/core/database.js +9 -18
  56. package/dist/core/detector.js +11 -21
  57. package/dist/core/git-utils.d.ts +12 -0
  58. package/dist/core/git-utils.js +87 -0
  59. package/dist/core/index.d.ts +3 -0
  60. package/dist/core/index.js +8 -5
  61. package/dist/core/scanner.js +3 -2
  62. package/dist/core/settings.d.ts +85 -0
  63. package/dist/core/settings.js +306 -9
  64. package/dist/core/workspace-utils.d.ts +37 -0
  65. package/dist/core/workspace-utils.js +143 -0
  66. package/dist/core-bridge.js +7 -17
  67. package/dist/electron/core/__tests__/database.test.js +7 -17
  68. package/dist/electron/core/__tests__/detector.test.js +7 -17
  69. package/dist/electron/core/__tests__/index.test.js +7 -17
  70. package/dist/electron/core/__tests__/scanner.test.js +7 -17
  71. package/dist/electron/core/__tests__/settings.test.js +7 -17
  72. package/dist/electron/core/backup-utils.d.ts +17 -0
  73. package/dist/electron/core/backup-utils.js +157 -0
  74. package/dist/electron/core/database.d.ts +1 -0
  75. package/dist/electron/core/database.js +9 -18
  76. package/dist/electron/core/detector.js +11 -21
  77. package/dist/electron/core/git-utils.d.ts +12 -0
  78. package/dist/electron/core/git-utils.js +87 -0
  79. package/dist/electron/core/index.d.ts +3 -0
  80. package/dist/electron/core/index.js +8 -5
  81. package/dist/electron/core/scanner.js +3 -2
  82. package/dist/electron/core/settings.d.ts +85 -0
  83. package/dist/electron/core/settings.js +306 -9
  84. package/dist/electron/core/workspace-utils.d.ts +37 -0
  85. package/dist/electron/core/workspace-utils.js +143 -0
  86. package/dist/electron/core.js +7 -17
  87. package/dist/electron/main.js +663 -33
  88. package/dist/electron/port-extractor.js +9 -18
  89. package/dist/electron/port-scanner.js +11 -20
  90. package/dist/electron/port-utils.js +5 -4
  91. package/dist/electron/preload.d.ts +27 -2
  92. package/dist/electron/preload.js +18 -2
  93. package/dist/electron/renderer/assets/index-B-etDnj2.js +64 -0
  94. package/dist/electron/renderer/assets/index-Bx18Cyic.js +64 -0
  95. package/dist/electron/renderer/assets/index-C8f5yNYe.js +64 -0
  96. package/dist/electron/renderer/assets/index-CIZ3Wl6c.css +1 -0
  97. package/dist/electron/renderer/assets/index-CJbsU9y8.css +1 -0
  98. package/dist/electron/renderer/assets/index-CopVNRnR.js +64 -0
  99. package/dist/electron/renderer/assets/index-DUvcepWm.js +64 -0
  100. package/dist/electron/renderer/assets/index-DWe2TQFv.css +1 -0
  101. package/dist/electron/renderer/assets/index-DZzB20Xf.css +1 -0
  102. package/dist/electron/renderer/assets/index-DknLdADV.js +63 -0
  103. package/dist/electron/renderer/assets/index-DocuD8Lk.js +64 -0
  104. package/dist/electron/renderer/assets/index-DyU-xfd8.css +1 -0
  105. package/dist/electron/renderer/assets/index-GwC-JVUy.css +1 -0
  106. package/dist/electron/renderer/assets/index-fehviker.js +63 -0
  107. package/dist/electron/renderer/index.html +2 -2
  108. package/dist/electron/script-runner.js +20 -29
  109. package/dist/index.js +395 -21
  110. package/dist/port-extractor.js +9 -18
  111. package/dist/port-scanner.js +11 -20
  112. package/dist/port-utils.js +5 -4
  113. package/dist/script-runner.js +20 -29
  114. package/dist/test-parser.js +3 -2
  115. package/package.json +3 -2
@@ -15,27 +15,19 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
15
15
  }) : function(o, v) {
16
16
  o["default"] = v;
17
17
  });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
35
25
  Object.defineProperty(exports, "__esModule", { value: true });
36
26
  const electron_1 = require("electron");
37
27
  const path = __importStar(require("path"));
38
28
  const fs = __importStar(require("fs"));
29
+ const os = __importStar(require("os"));
30
+ const http = __importStar(require("http"));
39
31
  const child_process_1 = require("child_process");
40
32
  const tail_1 = require("tail");
41
33
  const core_1 = require("./core");
@@ -68,17 +60,53 @@ else {
68
60
  height: 800,
69
61
  frame: false,
70
62
  titleBarStyle: 'hidden',
63
+ trafficLightPosition: { x: -100, y: -100 }, // Hide macOS traffic lights
71
64
  title: 'PROJAX UI',
72
65
  webPreferences: {
73
66
  preload: path.join(__dirname, 'preload.js'),
74
67
  nodeIntegration: false,
75
68
  contextIsolation: true,
69
+ webSecurity: true,
76
70
  },
71
+ show: false, // Don't show until ready
72
+ });
73
+ // Show window when ready to prevent white screen flash
74
+ mainWindow.once('ready-to-show', () => {
75
+ mainWindow?.show();
77
76
  });
78
77
  // Load the app
79
78
  if (isDev) {
80
- mainWindow.loadURL('http://localhost:7898');
81
- mainWindow.webContents.openDevTools();
79
+ // Wait for Vite dev server to be ready before loading
80
+ const checkServerAndLoad = (retries = 10) => {
81
+ const req = http.get('http://localhost:7898', (res) => {
82
+ console.log('Vite dev server is ready!');
83
+ mainWindow?.loadURL('http://localhost:7898');
84
+ mainWindow?.webContents.openDevTools();
85
+ });
86
+ req.on('error', (error) => {
87
+ if (retries > 0) {
88
+ console.log(`Vite dev server not ready (${retries} retries left), retrying in 1 second...`);
89
+ setTimeout(() => checkServerAndLoad(retries - 1), 1000);
90
+ }
91
+ else {
92
+ console.error('Failed to connect to Vite dev server after multiple retries');
93
+ console.error('Make sure Vite is running on port 7898');
94
+ mainWindow?.loadURL('http://localhost:7898'); // Try anyway
95
+ }
96
+ });
97
+ req.setTimeout(2000, () => {
98
+ req.destroy();
99
+ if (retries > 0) {
100
+ console.log(`Vite dev server timeout (${retries} retries left), retrying...`);
101
+ setTimeout(() => checkServerAndLoad(retries - 1), 1000);
102
+ }
103
+ else {
104
+ console.error('Failed to connect to Vite dev server - timeout');
105
+ mainWindow?.loadURL('http://localhost:7898'); // Try anyway
106
+ }
107
+ });
108
+ };
109
+ checkServerAndLoad();
82
110
  }
83
111
  else {
84
112
  // Try bundled renderer path first (when bundled in CLI: dist/electron/renderer/index.html)
@@ -113,6 +141,49 @@ else {
113
141
  mainWindow.on('closed', () => {
114
142
  mainWindow = null;
115
143
  });
144
+ // Handle page load errors
145
+ mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => {
146
+ console.error('Failed to load:', validatedURL, errorCode, errorDescription);
147
+ if (isDev && validatedURL === 'http://localhost:7898/') {
148
+ console.log('Retrying to load Vite dev server...');
149
+ setTimeout(() => {
150
+ mainWindow?.loadURL('http://localhost:7898');
151
+ }, 2000);
152
+ }
153
+ });
154
+ // Log when page finishes loading
155
+ mainWindow.webContents.on('did-finish-load', () => {
156
+ console.log('Page loaded successfully');
157
+ });
158
+ // Log console messages from renderer
159
+ mainWindow.webContents.on('console-message', (event, level, message, line, sourceId) => {
160
+ console.log(`[Renderer ${level}]:`, message, sourceId ? `(${sourceId}:${line})` : '');
161
+ });
162
+ // Log all console output from renderer
163
+ mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
164
+ if (isMainFrame) {
165
+ console.error('Main frame failed to load:', {
166
+ errorCode,
167
+ errorDescription,
168
+ validatedURL,
169
+ });
170
+ }
171
+ });
172
+ // Add keyboard shortcut to reload in dev mode
173
+ if (isDev) {
174
+ mainWindow.webContents.on('before-input-event', (event, input) => {
175
+ // Cmd+R or Ctrl+R to reload
176
+ if ((input.control || input.meta) && input.key.toLowerCase() === 'r') {
177
+ event.preventDefault();
178
+ mainWindow?.reload();
179
+ }
180
+ // Cmd+Shift+R or Ctrl+Shift+R to hard reload
181
+ if ((input.control || input.meta) && input.shift && input.key.toLowerCase() === 'r') {
182
+ event.preventDefault();
183
+ mainWindow?.webContents.reloadIgnoringCache();
184
+ }
185
+ });
186
+ }
116
187
  }
117
188
  // Start API server
118
189
  function startAPIServer() {
@@ -134,11 +205,18 @@ else {
134
205
  console.warn('API server not found. Some features may not work.');
135
206
  return;
136
207
  }
137
- console.log('Starting API server...');
208
+ console.log('Starting API server from:', apiPath);
209
+ // Kill any existing API server on the same port first
210
+ if (apiProcess) {
211
+ console.log('Killing existing API server...');
212
+ apiProcess.kill();
213
+ apiProcess = null;
214
+ }
138
215
  apiProcess = (0, child_process_1.spawn)('node', [apiPath], {
139
216
  detached: false,
140
217
  stdio: 'pipe',
141
218
  env: { ...process.env },
219
+ cwd: path.dirname(apiPath),
142
220
  });
143
221
  apiProcess.stdout?.on('data', (data) => {
144
222
  console.log(`[API] ${data.toString().trim()}`);
@@ -149,13 +227,135 @@ else {
149
227
  apiProcess.on('exit', (code) => {
150
228
  console.log(`API server exited with code ${code}`);
151
229
  apiProcess = null;
230
+ // Restart API server if it crashes (wait 2 seconds)
231
+ if (code !== 0) {
232
+ console.log('API server crashed, restarting in 2 seconds...');
233
+ setTimeout(() => {
234
+ startAPIServer();
235
+ }, 2000);
236
+ }
152
237
  });
153
238
  }
154
239
  catch (error) {
155
240
  console.error('Failed to start API server:', error);
156
241
  }
157
242
  }
243
+ function createMenu() {
244
+ const template = [
245
+ {
246
+ label: 'PROJAX UI',
247
+ submenu: [
248
+ { role: 'about', label: 'About PROJAX UI' },
249
+ { type: 'separator' },
250
+ { role: 'services', label: 'Services' },
251
+ { type: 'separator' },
252
+ { role: 'hide', label: 'Hide PROJAX UI' },
253
+ { role: 'hideOthers', label: 'Hide Others' },
254
+ { role: 'unhide', label: 'Show All' },
255
+ { type: 'separator' },
256
+ { role: 'quit', label: 'Quit PROJAX UI' },
257
+ ],
258
+ },
259
+ {
260
+ label: 'File',
261
+ submenu: [
262
+ {
263
+ label: 'New Project',
264
+ accelerator: 'CmdOrCtrl+N',
265
+ click: () => {
266
+ if (mainWindow) {
267
+ mainWindow.webContents.send('menu-action', 'new-project');
268
+ }
269
+ },
270
+ },
271
+ {
272
+ label: 'New Workspace',
273
+ accelerator: 'CmdOrCtrl+Shift+N',
274
+ click: () => {
275
+ if (mainWindow) {
276
+ mainWindow.webContents.send('menu-action', 'new-workspace');
277
+ }
278
+ },
279
+ },
280
+ { type: 'separator' },
281
+ {
282
+ label: 'Open Workspace',
283
+ accelerator: 'CmdOrCtrl+O',
284
+ click: () => {
285
+ if (mainWindow) {
286
+ mainWindow.webContents.send('menu-action', 'open-workspace');
287
+ }
288
+ },
289
+ },
290
+ {
291
+ label: 'Open Directory as New Project',
292
+ accelerator: 'CmdOrCtrl+Shift+O',
293
+ click: async () => {
294
+ if (mainWindow) {
295
+ const result = await electron_1.dialog.showOpenDialog(mainWindow, {
296
+ properties: ['openDirectory'],
297
+ });
298
+ if (!result.canceled && result.filePaths.length > 0) {
299
+ mainWindow.webContents.send('menu-action', 'open-directory-as-project', result.filePaths[0]);
300
+ }
301
+ }
302
+ },
303
+ },
304
+ { type: 'separator' },
305
+ { role: 'close', label: 'Close Window' },
306
+ ],
307
+ },
308
+ {
309
+ label: 'Edit',
310
+ submenu: [
311
+ { role: 'undo', label: 'Undo' },
312
+ { role: 'redo', label: 'Redo' },
313
+ { type: 'separator' },
314
+ { role: 'cut', label: 'Cut' },
315
+ { role: 'copy', label: 'Copy' },
316
+ { role: 'paste', label: 'Paste' },
317
+ { role: 'selectAll', label: 'Select All' },
318
+ ],
319
+ },
320
+ {
321
+ label: 'View',
322
+ submenu: [
323
+ { role: 'reload', label: 'Reload' },
324
+ { role: 'forceReload', label: 'Force Reload' },
325
+ { role: 'toggleDevTools', label: 'Toggle Developer Tools' },
326
+ { type: 'separator' },
327
+ { role: 'resetZoom', label: 'Actual Size' },
328
+ { role: 'zoomIn', label: 'Zoom In' },
329
+ { role: 'zoomOut', label: 'Zoom Out' },
330
+ { type: 'separator' },
331
+ { role: 'togglefullscreen', label: 'Toggle Full Screen' },
332
+ ],
333
+ },
334
+ {
335
+ label: 'Window',
336
+ submenu: [
337
+ { role: 'minimize', label: 'Minimize' },
338
+ { role: 'close', label: 'Close' },
339
+ ],
340
+ },
341
+ ];
342
+ // macOS specific menu adjustments
343
+ if (process.platform === 'darwin') {
344
+ template[0].label = 'PROJAX UI';
345
+ }
346
+ else {
347
+ // Windows/Linux: Remove the app menu and move items to File menu
348
+ template.shift();
349
+ const fileMenu = template[0];
350
+ if (fileMenu.submenu && Array.isArray(fileMenu.submenu)) {
351
+ fileMenu.submenu.push({ type: 'separator' }, { role: 'quit', label: 'Exit' });
352
+ }
353
+ }
354
+ const menu = electron_1.Menu.buildFromTemplate(template);
355
+ electron_1.Menu.setApplicationMenu(menu);
356
+ }
158
357
  electron_1.app.whenReady().then(() => {
358
+ createMenu();
159
359
  startAPIServer();
160
360
  createWindow();
161
361
  electron_1.app.on('activate', () => {
@@ -195,9 +395,42 @@ electron_1.ipcMain.handle('get-app-version', async () => {
195
395
  electron_1.ipcMain.handle('get-projects', async () => {
196
396
  try {
197
397
  console.log('Getting projects from database...');
198
- const projects = (0, core_1.getAllProjects)();
199
- console.log(`Found ${projects.length} project(s)`);
200
- return projects;
398
+ // Wait a bit for API server to be ready if it just started
399
+ // Try to get projects, with a retry if it fails
400
+ let projects = [];
401
+ let lastError = null;
402
+ for (let attempt = 0; attempt < 3; attempt++) {
403
+ try {
404
+ projects = (0, core_1.getAllProjects)();
405
+ console.log(`Found ${projects.length} project(s)`);
406
+ return projects;
407
+ }
408
+ catch (error) {
409
+ lastError = error instanceof Error ? error : new Error(String(error));
410
+ if (attempt < 2) {
411
+ // Wait a bit before retrying (API server might be starting)
412
+ await new Promise(resolve => setTimeout(resolve, 500));
413
+ }
414
+ }
415
+ }
416
+ // If all retries failed, try reading directly from database file as fallback
417
+ try {
418
+ const dataDir = path.join(os.homedir(), '.projax');
419
+ const dbPath = path.join(dataDir, 'data.json');
420
+ if (fs.existsSync(dbPath)) {
421
+ const dbContent = fs.readFileSync(dbPath, 'utf-8');
422
+ const db = JSON.parse(dbContent);
423
+ if (db.projects && Array.isArray(db.projects)) {
424
+ console.log(`Fallback: Found ${db.projects.length} project(s) from database file`);
425
+ return db.projects;
426
+ }
427
+ }
428
+ }
429
+ catch (fileError) {
430
+ console.warn('Could not read projects from database file:', fileError);
431
+ }
432
+ // If we still don't have projects, throw the original error
433
+ throw lastError || new Error('Failed to get projects');
201
434
  }
202
435
  catch (error) {
203
436
  console.error('Error getting projects:', error);
@@ -292,7 +525,7 @@ electron_1.ipcMain.handle('rename-project', async (_, projectId, newName) => {
292
525
  return db.updateProjectName(projectId, newName);
293
526
  });
294
527
  // Get project scripts
295
- electron_1.ipcMain.handle('get-project-scripts', async (_, projectPath) => {
528
+ electron_1.ipcMain.handle('get-project-scripts', async (_, projectPath, projectId) => {
296
529
  // Try bundled path first (when bundled in CLI: dist/electron/main.js -> dist/script-runner.js)
297
530
  // Then try local dev path (packages/desktop/dist/main.js -> packages/cli/dist/script-runner.js)
298
531
  const bundledScriptRunnerPath = path.join(__dirname, '..', 'script-runner.js');
@@ -305,7 +538,69 @@ electron_1.ipcMain.handle('get-project-scripts', async (_, projectPath) => {
305
538
  scriptRunnerPath = localScriptRunnerPath;
306
539
  }
307
540
  const { getProjectScripts } = await Promise.resolve(`${scriptRunnerPath}`).then(s => __importStar(require(s)));
308
- const result = getProjectScripts(projectPath);
541
+ // Get project settings if projectId is provided
542
+ // Read directly from database file instead of making HTTP requests
543
+ let scriptsPath = null;
544
+ if (projectId) {
545
+ try {
546
+ const dataDir = path.join(os.homedir(), '.projax');
547
+ const dbPath = path.join(dataDir, 'data.json');
548
+ // Try reading the file with a small retry in case it's being written
549
+ let db = null;
550
+ for (let attempt = 0; attempt < 3; attempt++) {
551
+ try {
552
+ if (fs.existsSync(dbPath)) {
553
+ const dbContent = fs.readFileSync(dbPath, 'utf-8');
554
+ db = JSON.parse(dbContent);
555
+ break; // Successfully read, exit retry loop
556
+ }
557
+ }
558
+ catch (readError) {
559
+ if (attempt < 2) {
560
+ // Wait a bit before retrying (only on first two attempts)
561
+ await new Promise(resolve => setTimeout(resolve, 100));
562
+ continue;
563
+ }
564
+ throw readError;
565
+ }
566
+ }
567
+ if (db && db.project_settings && Array.isArray(db.project_settings)) {
568
+ const settings = db.project_settings.find((ps) => ps.project_id === projectId);
569
+ if (settings && settings.scripts_path) {
570
+ scriptsPath = settings.scripts_path;
571
+ }
572
+ }
573
+ }
574
+ catch (error) {
575
+ // Silently fail - will use project root if settings can't be loaded
576
+ console.debug('Could not load project settings for scripts, using project root:', error);
577
+ }
578
+ }
579
+ // Use custom scripts path if set, otherwise use project root
580
+ let actualScriptsPath = projectPath;
581
+ if (scriptsPath) {
582
+ const joinedPath = path.join(projectPath, scriptsPath);
583
+ // Validate that the path exists
584
+ if (fs.existsSync(joinedPath)) {
585
+ const stats = fs.statSync(joinedPath);
586
+ if (stats.isDirectory()) {
587
+ // If it's a directory, use it directly
588
+ actualScriptsPath = joinedPath;
589
+ }
590
+ else if (stats.isFile()) {
591
+ // If it's a file, use the directory containing the file
592
+ actualScriptsPath = path.dirname(joinedPath);
593
+ }
594
+ else {
595
+ console.warn(`Custom scripts path is not a directory or file: ${joinedPath}, using project root instead`);
596
+ }
597
+ }
598
+ else {
599
+ console.warn(`Custom scripts path does not exist: ${joinedPath}, using project root instead`);
600
+ // Fall back to project root if custom path doesn't exist
601
+ }
602
+ }
603
+ const result = getProjectScripts(actualScriptsPath);
309
604
  // Convert Map to array for IPC serialization
310
605
  const scriptsArray = Array.from(result.scripts.entries()).map(([name, script]) => ({
311
606
  name,
@@ -537,11 +832,11 @@ electron_1.ipcMain.handle('open-url', async (_, url) => {
537
832
  }).unref();
538
833
  });
539
834
  // Open project in editor
540
- electron_1.ipcMain.handle('open-in-editor', async (_, projectPath) => {
835
+ electron_1.ipcMain.handle('open-in-editor', async (_, projectPath, projectId) => {
541
836
  // Try bundled path first (when bundled in CLI: dist/electron/main.js -> dist/core/settings)
542
837
  // Then try local dev path (packages/desktop/dist/main.js -> packages/core/dist/settings)
543
- const bundledSettingsPath = path.join(__dirname, '..', 'core', 'settings');
544
- const localSettingsPath = path.join(__dirname, '..', '..', '..', 'core', 'dist', 'settings');
838
+ const bundledSettingsPath = path.join(__dirname, 'core', 'settings');
839
+ const localSettingsPath = path.join(__dirname, '..', '..', 'core', 'dist', 'settings');
545
840
  let settingsPath;
546
841
  if (fs.existsSync(bundledSettingsPath + '.js')) {
547
842
  settingsPath = bundledSettingsPath;
@@ -550,7 +845,41 @@ electron_1.ipcMain.handle('open-in-editor', async (_, projectPath) => {
550
845
  settingsPath = localSettingsPath;
551
846
  }
552
847
  const { getEditorSettings } = require(settingsPath);
553
- const editorSettings = getEditorSettings();
848
+ const globalEditorSettings = getEditorSettings();
849
+ // Check for project-specific editor settings
850
+ let editorSettings = globalEditorSettings;
851
+ if (projectId) {
852
+ try {
853
+ const ports = [38124, 38125, 38126, 38127, 38128, 3001];
854
+ let apiBaseUrl = '';
855
+ for (const port of ports) {
856
+ try {
857
+ const response = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(500) });
858
+ if (response.ok) {
859
+ apiBaseUrl = `http://localhost:${port}/api`;
860
+ break;
861
+ }
862
+ }
863
+ catch {
864
+ continue;
865
+ }
866
+ }
867
+ if (apiBaseUrl) {
868
+ const settingsResponse = await fetch(`${apiBaseUrl}/projects/${projectId}/settings`);
869
+ if (settingsResponse.ok) {
870
+ const projectSettings = await settingsResponse.json();
871
+ if (projectSettings.editor && projectSettings.editor.type) {
872
+ // Use project-specific editor
873
+ editorSettings = projectSettings.editor;
874
+ }
875
+ }
876
+ }
877
+ }
878
+ catch (error) {
879
+ console.error('Error loading project editor settings:', error);
880
+ // Fall back to global settings
881
+ }
882
+ }
554
883
  let command;
555
884
  let args = [projectPath];
556
885
  if (editorSettings.type === 'custom' && editorSettings.customPath) {
@@ -580,6 +909,96 @@ electron_1.ipcMain.handle('open-in-editor', async (_, projectPath) => {
580
909
  stdio: 'ignore',
581
910
  }).unref();
582
911
  });
912
+ // Open workspace in editor
913
+ electron_1.ipcMain.handle('open-workspace', async (_, workspaceId) => {
914
+ try {
915
+ // Get workspace file path from API (ensures file exists)
916
+ const ports = [38124, 38125, 38126, 38127, 38128, 3001];
917
+ let apiBaseUrl = '';
918
+ for (const port of ports) {
919
+ try {
920
+ const response = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(500) });
921
+ if (response.ok) {
922
+ apiBaseUrl = `http://localhost:${port}/api`;
923
+ break;
924
+ }
925
+ }
926
+ catch {
927
+ continue;
928
+ }
929
+ }
930
+ if (!apiBaseUrl) {
931
+ throw new Error('API server not found');
932
+ }
933
+ // Get workspace file path (this will generate it if needed)
934
+ const response = await fetch(`${apiBaseUrl}/workspaces/${workspaceId}/file-path`);
935
+ if (!response.ok) {
936
+ throw new Error('Failed to get workspace file path');
937
+ }
938
+ const data = await response.json();
939
+ const workspace_file_path = data.workspace_file_path;
940
+ if (!fs.existsSync(workspace_file_path)) {
941
+ throw new Error('Workspace file does not exist');
942
+ }
943
+ // Open workspace file in editor (workspace files are opened with the workspace flag)
944
+ const bundledSettingsPath = path.join(__dirname, 'core', 'settings');
945
+ const localSettingsPath = path.join(__dirname, '..', '..', 'core', 'dist', 'settings');
946
+ let settingsPath;
947
+ if (fs.existsSync(bundledSettingsPath + '.js')) {
948
+ settingsPath = bundledSettingsPath;
949
+ }
950
+ else {
951
+ settingsPath = localSettingsPath;
952
+ }
953
+ const { getEditorSettings } = require(settingsPath);
954
+ const editorSettings = getEditorSettings();
955
+ let command;
956
+ let args = [];
957
+ if (editorSettings.type === 'custom' && editorSettings.customPath) {
958
+ command = editorSettings.customPath;
959
+ // For custom editors, try workspace file as argument
960
+ args = [workspace_file_path];
961
+ }
962
+ else {
963
+ switch (editorSettings.type) {
964
+ case 'vscode':
965
+ case 'cursor':
966
+ case 'windsurf':
967
+ // VS Code, Cursor, and Windsurf support opening workspace files directly
968
+ command = editorSettings.type === 'vscode' ? 'code' : editorSettings.type === 'cursor' ? 'cursor' : 'windsurf';
969
+ args = [workspace_file_path];
970
+ break;
971
+ case 'zed':
972
+ // Zed doesn't support workspace files, open the first project folder instead
973
+ const workspaceContent = JSON.parse(fs.readFileSync(workspace_file_path, 'utf-8'));
974
+ if (workspaceContent.folders && workspaceContent.folders.length > 0) {
975
+ const firstFolder = workspaceContent.folders[0];
976
+ const folderPath = path.isAbsolute(firstFolder.path)
977
+ ? firstFolder.path
978
+ : path.resolve(path.dirname(workspace_file_path), firstFolder.path);
979
+ command = 'zed';
980
+ args = [folderPath];
981
+ }
982
+ else {
983
+ throw new Error('Workspace has no folders to open');
984
+ }
985
+ break;
986
+ default:
987
+ command = 'code';
988
+ args = [workspace_file_path];
989
+ }
990
+ }
991
+ const { spawn } = require('child_process');
992
+ spawn(command, args, {
993
+ detached: true,
994
+ stdio: 'ignore',
995
+ }).unref();
996
+ }
997
+ catch (error) {
998
+ console.error('Error opening workspace:', error);
999
+ throw error;
1000
+ }
1001
+ });
583
1002
  // Open project directory in file manager
584
1003
  electron_1.ipcMain.handle('open-in-files', async (_, projectPath) => {
585
1004
  try {
@@ -590,12 +1009,23 @@ electron_1.ipcMain.handle('open-in-files', async (_, projectPath) => {
590
1009
  throw error;
591
1010
  }
592
1011
  });
1012
+ // Open file path in file manager (reveals file in finder/explorer)
1013
+ electron_1.ipcMain.handle('open-file-path', async (_, filePath) => {
1014
+ try {
1015
+ // Use shell.showItemInFolder to reveal the file in the file manager
1016
+ electron_1.shell.showItemInFolder(filePath);
1017
+ }
1018
+ catch (error) {
1019
+ console.error('Error opening file path:', error);
1020
+ throw error;
1021
+ }
1022
+ });
593
1023
  // Get settings
594
1024
  electron_1.ipcMain.handle('get-settings', async () => {
595
1025
  try {
596
1026
  // Load settings module directly
597
- const bundledSettingsPath = path.join(__dirname, '..', 'core', 'settings');
598
- const localSettingsPath = path.join(__dirname, '..', '..', '..', 'core', 'dist', 'settings');
1027
+ const bundledSettingsPath = path.join(__dirname, 'core', 'settings');
1028
+ const localSettingsPath = path.join(__dirname, '..', '..', 'core', 'dist', 'settings');
599
1029
  let settingsPath;
600
1030
  if (fs.existsSync(bundledSettingsPath + '.js')) {
601
1031
  settingsPath = bundledSettingsPath;
@@ -615,8 +1045,8 @@ electron_1.ipcMain.handle('get-settings', async () => {
615
1045
  electron_1.ipcMain.handle('save-settings', async (_, settings) => {
616
1046
  try {
617
1047
  // Load settings module directly
618
- const bundledSettingsPath = path.join(__dirname, '..', 'core', 'settings');
619
- const localSettingsPath = path.join(__dirname, '..', '..', '..', 'core', 'dist', 'settings');
1048
+ const bundledSettingsPath = path.join(__dirname, 'core', 'settings');
1049
+ const localSettingsPath = path.join(__dirname, '..', '..', 'core', 'dist', 'settings');
620
1050
  let settingsPath;
621
1051
  if (fs.existsSync(bundledSettingsPath + '.js')) {
622
1052
  settingsPath = bundledSettingsPath;
@@ -722,3 +1152,203 @@ electron_1.ipcMain.handle('unwatch-process-output', async (_, pid) => {
722
1152
  }
723
1153
  return { success: false };
724
1154
  });
1155
+ // Workspace handlers
1156
+ electron_1.ipcMain.handle('get-workspaces', async () => {
1157
+ try {
1158
+ // Try to find the correct API port
1159
+ const ports = [38124, 38125, 38126, 38127, 38128, 3001];
1160
+ let apiBaseUrl = '';
1161
+ for (const port of ports) {
1162
+ try {
1163
+ const response = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(500) });
1164
+ if (response.ok) {
1165
+ apiBaseUrl = `http://localhost:${port}/api`;
1166
+ break;
1167
+ }
1168
+ }
1169
+ catch {
1170
+ continue;
1171
+ }
1172
+ }
1173
+ // Try fetching from API with retries
1174
+ if (apiBaseUrl) {
1175
+ for (let i = 0; i < 3; i++) {
1176
+ try {
1177
+ const response = await fetch(`${apiBaseUrl}/workspaces`);
1178
+ if (response.ok) {
1179
+ const workspaces = await response.json();
1180
+ console.log(`[main] Fetched ${workspaces.length} workspace(s) from API.`);
1181
+ return workspaces;
1182
+ }
1183
+ }
1184
+ catch (error) {
1185
+ console.debug(`[main] API fetch attempt ${i + 1} failed:`, error);
1186
+ if (i < 2) {
1187
+ await new Promise(resolve => setTimeout(resolve, 500)); // Wait 500ms before retry
1188
+ }
1189
+ }
1190
+ }
1191
+ }
1192
+ // Fallback to reading directly from database file if API is not available or failed
1193
+ try {
1194
+ console.warn('[main] API not available or failed after retries. Reading workspaces directly from database file.');
1195
+ const dataDir = path.join(os.homedir(), '.projax');
1196
+ const dbPath = path.join(dataDir, 'data.json');
1197
+ if (fs.existsSync(dbPath)) {
1198
+ const dbContent = fs.readFileSync(dbPath, 'utf-8');
1199
+ const db = JSON.parse(dbContent);
1200
+ const workspaces = db.workspaces || [];
1201
+ console.log(`[main] Found ${workspaces.length} workspace(s) in database file.`);
1202
+ return workspaces;
1203
+ }
1204
+ }
1205
+ catch (fileError) {
1206
+ console.warn('[main] Could not read workspaces from database file:', fileError);
1207
+ }
1208
+ // If we still don't have workspaces, return empty array
1209
+ console.warn('[main] No workspaces found, returning empty array.');
1210
+ return [];
1211
+ }
1212
+ catch (error) {
1213
+ console.error('Error getting workspaces:', error);
1214
+ // Return empty array instead of throwing to prevent app crash
1215
+ return [];
1216
+ }
1217
+ });
1218
+ electron_1.ipcMain.handle('add-workspace', async (_, workspace) => {
1219
+ try {
1220
+ // Try to find the correct API port
1221
+ const ports = [38124, 38125, 38126, 38127, 38128, 3001];
1222
+ let apiBaseUrl = '';
1223
+ for (const port of ports) {
1224
+ try {
1225
+ const response = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(500) });
1226
+ if (response.ok) {
1227
+ apiBaseUrl = `http://localhost:${port}/api`;
1228
+ break;
1229
+ }
1230
+ }
1231
+ catch {
1232
+ continue;
1233
+ }
1234
+ }
1235
+ if (!apiBaseUrl) {
1236
+ throw new Error('API server not found');
1237
+ }
1238
+ const response = await fetch(`${apiBaseUrl}/workspaces`, {
1239
+ method: 'POST',
1240
+ headers: { 'Content-Type': 'application/json' },
1241
+ body: JSON.stringify(workspace),
1242
+ });
1243
+ if (!response.ok) {
1244
+ const errorData = await response.json();
1245
+ throw new Error(errorData.error || 'Failed to add workspace');
1246
+ }
1247
+ return await response.json();
1248
+ }
1249
+ catch (error) {
1250
+ console.error('Error adding workspace:', error);
1251
+ throw error;
1252
+ }
1253
+ });
1254
+ electron_1.ipcMain.handle('remove-workspace', async (_, workspaceId) => {
1255
+ try {
1256
+ const response = await fetch(`http://localhost:3001/api/workspaces/${workspaceId}`, {
1257
+ method: 'DELETE',
1258
+ });
1259
+ if (!response.ok)
1260
+ throw new Error('Failed to remove workspace');
1261
+ }
1262
+ catch (error) {
1263
+ console.error('Error removing workspace:', error);
1264
+ throw error;
1265
+ }
1266
+ });
1267
+ // Backup handlers
1268
+ electron_1.ipcMain.handle('create-backup', async (_, outputPath) => {
1269
+ try {
1270
+ // Try to load from dist first, then src
1271
+ const backupUtilsPath = path.join(__dirname, '..', '..', 'core', 'dist', 'backup-utils.js');
1272
+ const backupUtilsSrcPath = path.join(__dirname, '..', '..', 'core', 'src', 'backup-utils.ts');
1273
+ let backupUtils;
1274
+ if (fs.existsSync(backupUtilsPath)) {
1275
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1276
+ backupUtils = require(backupUtilsPath);
1277
+ }
1278
+ else if (fs.existsSync(backupUtilsSrcPath.replace('.ts', '.js'))) {
1279
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1280
+ backupUtils = require(backupUtilsSrcPath.replace('.ts', '.js'));
1281
+ }
1282
+ else {
1283
+ throw new Error('Backup utils not found');
1284
+ }
1285
+ const { createBackup } = backupUtils;
1286
+ const backupPath = await createBackup(outputPath);
1287
+ return { success: true, backup_path: backupPath };
1288
+ }
1289
+ catch (error) {
1290
+ console.error('Error creating backup:', error);
1291
+ throw error;
1292
+ }
1293
+ });
1294
+ electron_1.ipcMain.handle('restore-backup', async (_, backupPath) => {
1295
+ try {
1296
+ // Try to load from dist first, then src
1297
+ const backupUtilsPath = path.join(__dirname, '..', '..', 'core', 'dist', 'backup-utils.js');
1298
+ const backupUtilsSrcPath = path.join(__dirname, '..', '..', 'core', 'src', 'backup-utils.ts');
1299
+ let backupUtils;
1300
+ if (fs.existsSync(backupUtilsPath)) {
1301
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1302
+ backupUtils = require(backupUtilsPath);
1303
+ }
1304
+ else if (fs.existsSync(backupUtilsSrcPath.replace('.ts', '.js'))) {
1305
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1306
+ backupUtils = require(backupUtilsSrcPath.replace('.ts', '.js'));
1307
+ }
1308
+ else {
1309
+ throw new Error('Backup utils not found');
1310
+ }
1311
+ const { restoreBackup } = backupUtils;
1312
+ await restoreBackup(backupPath);
1313
+ return { success: true };
1314
+ }
1315
+ catch (error) {
1316
+ console.error('Error restoring backup:', error);
1317
+ throw error;
1318
+ }
1319
+ });
1320
+ // File dialog handlers
1321
+ electron_1.ipcMain.handle('show-save-dialog', async (_, options) => {
1322
+ if (!mainWindow)
1323
+ return { canceled: true };
1324
+ const result = await electron_1.dialog.showSaveDialog(mainWindow, {
1325
+ title: options.title || 'Save File',
1326
+ defaultPath: options.defaultPath,
1327
+ filters: options.filters || [{ name: 'All Files', extensions: ['*'] }],
1328
+ });
1329
+ return result;
1330
+ });
1331
+ electron_1.ipcMain.handle('show-open-dialog', async (_, options) => {
1332
+ if (!mainWindow)
1333
+ return { canceled: true };
1334
+ const result = await electron_1.dialog.showOpenDialog(mainWindow, {
1335
+ title: options.title || 'Open File',
1336
+ defaultPath: options.defaultPath,
1337
+ filters: options.filters || [{ name: 'All Files', extensions: ['*'] }],
1338
+ properties: options.properties || ['openFile'],
1339
+ });
1340
+ return result;
1341
+ });
1342
+ electron_1.ipcMain.handle('select-file', async (_, options) => {
1343
+ if (!mainWindow)
1344
+ return null;
1345
+ const result = await electron_1.dialog.showOpenDialog(mainWindow, {
1346
+ title: options.title || 'Select File',
1347
+ defaultPath: options.defaultPath,
1348
+ filters: options.filters || [{ name: 'All Files', extensions: ['*'] }],
1349
+ properties: ['openFile'],
1350
+ });
1351
+ if (result.canceled || result.filePaths.length === 0)
1352
+ return null;
1353
+ return result.filePaths[0];
1354
+ });