projax 3.3.51 → 3.3.52

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 (162) hide show
  1. package/README.md +0 -73
  2. package/coverage/core-bridge.ts.html +24 -3
  3. package/coverage/index.html +34 -19
  4. package/coverage/lcov-report/core-bridge.ts.html +24 -3
  5. package/coverage/lcov-report/index.html +34 -19
  6. package/coverage/lcov-report/port-extractor.ts.html +1 -1
  7. package/coverage/lcov-report/port-scanner.ts.html +3 -3
  8. package/coverage/lcov-report/port-utils.ts.html +1 -1
  9. package/coverage/lcov-report/script-runner.ts.html +302 -11
  10. package/coverage/lcov-report/test-parser.ts.html +799 -0
  11. package/coverage/lcov.info +270 -49
  12. package/coverage/port-extractor.ts.html +1 -1
  13. package/coverage/port-scanner.ts.html +3 -3
  14. package/coverage/port-utils.ts.html +1 -1
  15. package/coverage/script-runner.ts.html +302 -11
  16. package/coverage/test-parser.ts.html +799 -0
  17. package/dist/__tests__/port-scanner.test.js +17 -7
  18. package/dist/__tests__/script-runner.test.js +17 -7
  19. package/dist/api/__tests__/database.test.js +17 -7
  20. package/dist/api/__tests__/database.test.js.map +1 -1
  21. package/dist/api/__tests__/routes.test.js +18 -7
  22. package/dist/api/__tests__/routes.test.js.map +1 -1
  23. package/dist/api/__tests__/scanner.test.js +18 -7
  24. package/dist/api/__tests__/scanner.test.js.map +1 -1
  25. package/dist/api/database.d.ts +0 -8
  26. package/dist/api/database.d.ts.map +1 -1
  27. package/dist/api/database.js +24 -57
  28. package/dist/api/database.js.map +1 -1
  29. package/dist/api/index.js +19 -9
  30. package/dist/api/index.js.map +1 -1
  31. package/dist/api/migrate.js +1 -2
  32. package/dist/api/migrate.js.map +1 -1
  33. package/dist/api/package.json +6 -3
  34. package/dist/api/routes/backup.d.ts +2 -1
  35. package/dist/api/routes/backup.d.ts.map +1 -1
  36. package/dist/api/routes/backup.js.map +1 -1
  37. package/dist/api/routes/index.d.ts +2 -1
  38. package/dist/api/routes/index.d.ts.map +1 -1
  39. package/dist/api/routes/index.js +0 -2
  40. package/dist/api/routes/index.js.map +1 -1
  41. package/dist/api/routes/projects.d.ts +2 -1
  42. package/dist/api/routes/projects.d.ts.map +1 -1
  43. package/dist/api/routes/projects.js +17 -7
  44. package/dist/api/routes/projects.js.map +1 -1
  45. package/dist/api/routes/settings.d.ts +2 -1
  46. package/dist/api/routes/settings.d.ts.map +1 -1
  47. package/dist/api/routes/settings.js +22 -57
  48. package/dist/api/routes/settings.js.map +1 -1
  49. package/dist/api/routes/workspaces.d.ts +2 -1
  50. package/dist/api/routes/workspaces.d.ts.map +1 -1
  51. package/dist/api/routes/workspaces.js +21 -96
  52. package/dist/api/routes/workspaces.js.map +1 -1
  53. package/dist/api/services/scanner.js +19 -10
  54. package/dist/api/services/scanner.js.map +1 -1
  55. package/dist/api/services/test-parser.js +2 -3
  56. package/dist/api/services/test-parser.js.map +1 -1
  57. package/dist/api/types.d.ts +0 -5
  58. package/dist/api/types.d.ts.map +1 -1
  59. package/dist/core/__tests__/database.test.js +17 -7
  60. package/dist/core/__tests__/detector.test.js +17 -7
  61. package/dist/core/__tests__/index.test.js +18 -7
  62. package/dist/core/__tests__/scanner.test.js +18 -7
  63. package/dist/core/__tests__/settings.test.js +18 -7
  64. package/dist/core/backup-utils.js +20 -11
  65. package/dist/core/database.js +18 -9
  66. package/dist/core/detector.js +21 -11
  67. package/dist/core/git-utils.js +19 -10
  68. package/dist/core/index.js +5 -5
  69. package/dist/core/scanner.js +2 -3
  70. package/dist/core/settings.d.ts +0 -85
  71. package/dist/core/settings.js +9 -306
  72. package/dist/core/workspace-utils.js +20 -11
  73. package/dist/core-bridge.js +22 -8
  74. package/dist/electron/core/__tests__/database.test.js +17 -7
  75. package/dist/electron/core/__tests__/detector.test.js +17 -7
  76. package/dist/electron/core/__tests__/index.test.js +18 -7
  77. package/dist/electron/core/__tests__/scanner.test.js +18 -7
  78. package/dist/electron/core/__tests__/settings.test.js +18 -7
  79. package/dist/electron/core/backup-utils.js +20 -11
  80. package/dist/electron/core/database.js +18 -9
  81. package/dist/electron/core/detector.js +21 -11
  82. package/dist/electron/core/git-utils.js +19 -10
  83. package/dist/electron/core/index.js +5 -5
  84. package/dist/electron/core/scanner.js +2 -3
  85. package/dist/electron/core/settings.d.ts +0 -85
  86. package/dist/electron/core/settings.js +9 -306
  87. package/dist/electron/core/workspace-utils.js +20 -11
  88. package/dist/electron/core.js +22 -8
  89. package/dist/electron/main.js +143 -444
  90. package/dist/electron/port-extractor.js +18 -9
  91. package/dist/electron/port-scanner.js +21 -12
  92. package/dist/electron/port-utils.js +4 -5
  93. package/dist/electron/preload.d.ts +2 -13
  94. package/dist/electron/preload.js +2 -9
  95. package/dist/electron/renderer/assets/index-BjZn_mEF.js +66 -0
  96. package/dist/electron/renderer/assets/index-CZmDxbJO.js +66 -0
  97. package/dist/electron/renderer/assets/{index-DWe2TQFv.css → index-DfocdjIj.css} +1 -1
  98. package/dist/electron/renderer/index.html +2 -2
  99. package/dist/electron/script-runner.js +29 -20
  100. package/dist/index.js +37 -134
  101. package/dist/port-extractor.js +18 -9
  102. package/dist/port-scanner.js +21 -12
  103. package/dist/port-utils.js +4 -5
  104. package/dist/prxi.d.ts +1 -0
  105. package/dist/prxi.js +1106 -0
  106. package/dist/prxi.tsx +6 -6
  107. package/dist/script-runner.js +29 -20
  108. package/dist/test-parser.js +2 -3
  109. package/jest.config.js +8 -0
  110. package/package.json +9 -6
  111. package/dist/api/routes/mcp.d.ts +0 -3
  112. package/dist/api/routes/mcp.d.ts.map +0 -1
  113. package/dist/api/routes/mcp.js +0 -147
  114. package/dist/api/routes/mcp.js.map +0 -1
  115. package/dist/electron/renderer/assets/index-59AhiV_K.css +0 -1
  116. package/dist/electron/renderer/assets/index-A04svynq.js +0 -62
  117. package/dist/electron/renderer/assets/index-B-etDnj2.js +0 -64
  118. package/dist/electron/renderer/assets/index-BGodNljq.js +0 -62
  119. package/dist/electron/renderer/assets/index-Bx18Cyic.js +0 -64
  120. package/dist/electron/renderer/assets/index-ByBOaxqv.js +0 -62
  121. package/dist/electron/renderer/assets/index-ByHY-x-j.js +0 -62
  122. package/dist/electron/renderer/assets/index-C1SRt6Jx.js +0 -62
  123. package/dist/electron/renderer/assets/index-C8f5yNYe.js +0 -64
  124. package/dist/electron/renderer/assets/index-C9Fo49a8.js +0 -61
  125. package/dist/electron/renderer/assets/index-CGx7K7jh.js +0 -62
  126. package/dist/electron/renderer/assets/index-CIZ3Wl6c.css +0 -1
  127. package/dist/electron/renderer/assets/index-CJbsU9y8.css +0 -1
  128. package/dist/electron/renderer/assets/index-CJrLunKK.js +0 -62
  129. package/dist/electron/renderer/assets/index-CQTleudf.css +0 -1
  130. package/dist/electron/renderer/assets/index-CQcilqlv.js +0 -62
  131. package/dist/electron/renderer/assets/index-CS-85xbL.css +0 -1
  132. package/dist/electron/renderer/assets/index-CYph0WPA.js +0 -62
  133. package/dist/electron/renderer/assets/index-C_WSLD6y.css +0 -1
  134. package/dist/electron/renderer/assets/index-CgB-tTpV.js +0 -62
  135. package/dist/electron/renderer/assets/index-ChoTzPLo.css +0 -1
  136. package/dist/electron/renderer/assets/index-CopVNRnR.js +0 -64
  137. package/dist/electron/renderer/assets/index-D1jmaGv5.css +0 -1
  138. package/dist/electron/renderer/assets/index-D2AOB6Er.js +0 -62
  139. package/dist/electron/renderer/assets/index-DAfjuYKX.js +0 -61
  140. package/dist/electron/renderer/assets/index-DEOOHPEi.css +0 -1
  141. package/dist/electron/renderer/assets/index-DTtg6XrF.css +0 -1
  142. package/dist/electron/renderer/assets/index-DUvcepWm.js +0 -64
  143. package/dist/electron/renderer/assets/index-DVWDlM1D.js +0 -62
  144. package/dist/electron/renderer/assets/index-DZzB20Xf.css +0 -1
  145. package/dist/electron/renderer/assets/index-Dk0EQt0u.css +0 -1
  146. package/dist/electron/renderer/assets/index-DknLdADV.js +0 -63
  147. package/dist/electron/renderer/assets/index-DocuD8Lk.js +0 -64
  148. package/dist/electron/renderer/assets/index-DwRy5FqP.js +0 -62
  149. package/dist/electron/renderer/assets/index-DyU-xfd8.css +0 -1
  150. package/dist/electron/renderer/assets/index-GwC-JVUy.css +0 -1
  151. package/dist/electron/renderer/assets/index-JXrtTB1F.js +0 -63
  152. package/dist/electron/renderer/assets/index-Ocrdv8Lb.css +0 -1
  153. package/dist/electron/renderer/assets/index-R-HsWJ0K.js +0 -62
  154. package/dist/electron/renderer/assets/index-Ytah0wbZ.js +0 -62
  155. package/dist/electron/renderer/assets/index-ZVyXUshO.css +0 -1
  156. package/dist/electron/renderer/assets/index-Z_8dJn3i.js +0 -62
  157. package/dist/electron/renderer/assets/index-fehviker.js +0 -63
  158. package/dist/electron/renderer/assets/index-nts9ST-M.js +0 -62
  159. package/dist/electron/renderer/assets/index-q8NVIH3g.css +0 -1
  160. package/dist/electron/renderer/assets/index-thUWIXon.js +0 -62
  161. package/dist/electron/renderer/assets/index-tuQmrwcm.css +0 -1
  162. package/dist/prxi/src/index.tsx +0 -1370
@@ -1,1370 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { render, Box, Text, useInput, useApp, useFocus, useFocusManager } from 'ink';
3
- import {
4
- getDatabaseManager,
5
- getAllProjects,
6
- scanProject,
7
- Project,
8
- } from '../../cli/src/core-bridge';
9
- import { getProjectScripts, getRunningProcessesClean, runScriptInBackground, stopScript } from '../../cli/src/script-runner';
10
- import { spawn } from 'child_process';
11
- import * as path from 'path';
12
- import * as os from 'os';
13
-
14
- // Color scheme matching desktop app
15
- const colors = {
16
- bgPrimary: '#0d1117',
17
- bgSecondary: '#161b22',
18
- bgTertiary: '#1c2128',
19
- bgHover: '#21262d',
20
- borderColor: '#30363d',
21
- textPrimary: '#c9d1d9',
22
- textSecondary: '#8b949e',
23
- textTertiary: '#6e7681',
24
- accentCyan: '#39c5cf',
25
- accentBlue: '#58a6ff',
26
- accentGreen: '#3fb950',
27
- accentPurple: '#bc8cff',
28
- accentOrange: '#ffa657',
29
- };
30
-
31
- // Helper function to get display path
32
- function getDisplayPath(fullPath: string): string {
33
- const parts = fullPath.split('/').filter(Boolean);
34
- if (parts.length === 0) return fullPath;
35
-
36
- const lastDir = parts[parts.length - 1];
37
-
38
- // If last directory is "src", go one up
39
- if (lastDir === 'src' && parts.length > 1) {
40
- return parts[parts.length - 2];
41
- }
42
-
43
- return lastDir;
44
- }
45
-
46
- // Helper function to truncate text
47
- function truncateText(text: string, maxLength: number): string {
48
- if (text.length <= maxLength) return text;
49
- return text.substring(0, maxLength - 3) + '...';
50
- }
51
-
52
- interface HelpModalProps {
53
- onClose: () => void;
54
- }
55
-
56
- const HelpModal: React.FC<HelpModalProps> = ({ onClose }) => {
57
- useInput((input: string, key: any) => {
58
- if (input === 'q' || key.escape || key.return) {
59
- onClose();
60
- }
61
- });
62
-
63
- return (
64
- <Box
65
- flexDirection="column"
66
- borderStyle="round"
67
- borderColor={colors.accentCyan}
68
- padding={1}
69
- width={70}
70
- >
71
- <Text bold color={colors.accentCyan}>
72
- PROJAX Terminal UI - Help
73
- </Text>
74
- <Text> </Text>
75
- <Text color={colors.accentCyan}>Navigation:</Text>
76
- <Text> ↑/k Move up in project list</Text>
77
- <Text> ↓/j Move down in project list</Text>
78
- <Text> Tab/←→ Switch between list and details</Text>
79
- <Text> </Text>
80
- <Text color={colors.accentCyan}>List Panel Actions:</Text>
81
- <Text> / Search projects (fuzzy search)</Text>
82
- <Text> s Scan selected project for tests</Text>
83
- <Text> </Text>
84
- <Text color={colors.accentCyan}>Details Panel Actions:</Text>
85
- <Text> ↑↓/kj Scroll details</Text>
86
- <Text> e Edit project name</Text>
87
- <Text> t Add/edit tags</Text>
88
- <Text> o Open project in editor</Text>
89
- <Text> f Open project directory</Text>
90
- <Text> u Show detected URLs</Text>
91
- <Text> s Scan project for tests</Text>
92
- <Text> p Scan ports for project</Text>
93
- <Text> r Show scripts (use CLI to run)</Text>
94
- <Text> x Stop all scripts for project</Text>
95
- <Text> d Delete project</Text>
96
- <Text> </Text>
97
- <Text color={colors.accentCyan}>Editing:</Text>
98
- <Text> Enter Save changes</Text>
99
- <Text> Esc Cancel editing</Text>
100
- <Text> </Text>
101
- <Text color={colors.accentCyan}>General:</Text>
102
- <Text> q/Esc Quit</Text>
103
- <Text> ? Show this help</Text>
104
- <Text> </Text>
105
- <Text color={colors.textSecondary}>Press any key to close...</Text>
106
- </Box>
107
- );
108
- };
109
-
110
- interface LoadingModalProps {
111
- message: string;
112
- }
113
-
114
- const LoadingModal: React.FC<LoadingModalProps> = ({ message }) => {
115
- return (
116
- <Box
117
- flexDirection="column"
118
- borderStyle="round"
119
- borderColor={colors.accentCyan}
120
- padding={1}
121
- width={40}
122
- >
123
- <Text>{message}</Text>
124
- <Text color={colors.textSecondary}>Please wait...</Text>
125
- </Box>
126
- );
127
- };
128
-
129
- interface ErrorModalProps {
130
- message: string;
131
- onClose: () => void;
132
- }
133
-
134
- const ErrorModal: React.FC<ErrorModalProps> = ({ message, onClose }) => {
135
- useInput((input: string, key: any) => {
136
- if (key.escape || key.return) {
137
- onClose();
138
- }
139
- });
140
-
141
- return (
142
- <Box
143
- flexDirection="column"
144
- borderStyle="round"
145
- borderColor="#f85149"
146
- padding={1}
147
- width={60}
148
- >
149
- <Text color="#f85149" bold>
150
- Error
151
- </Text>
152
- <Text> </Text>
153
- <Text>{message}</Text>
154
- <Text> </Text>
155
- <Text color={colors.textSecondary}>Press any key to close...</Text>
156
- </Box>
157
- );
158
- };
159
-
160
- interface ProjectListProps {
161
- projects: Project[];
162
- selectedIndex: number;
163
- runningProcesses: any[];
164
- isFocused: boolean;
165
- height: number;
166
- scrollOffset: number;
167
- }
168
-
169
- const ProjectListComponent: React.FC<ProjectListProps> = ({
170
- projects,
171
- selectedIndex,
172
- runningProcesses,
173
- isFocused,
174
- height,
175
- scrollOffset,
176
- }) => {
177
- const { focus } = useFocus({ id: 'projectList' });
178
-
179
- // Calculate visible range
180
- const startIndex = Math.max(0, scrollOffset);
181
- const hasMoreAbove = startIndex > 0;
182
- const headerHeight = 1; // "Projects (12)" line
183
- const paddingHeight = 2; // top + bottom padding
184
- const scrollIndicatorHeight = (hasMoreAbove ? 1 : 0) + 1; // "more above" + "more below" (always reserve space)
185
- const visibleHeight = Math.max(5, height - paddingHeight - headerHeight - scrollIndicatorHeight);
186
- const endIndex = Math.min(projects.length, startIndex + visibleHeight);
187
- const visibleProjects = projects.slice(startIndex, endIndex);
188
- const hasMoreBelow = endIndex < projects.length;
189
-
190
- return (
191
- <Box
192
- flexDirection="column"
193
- width="35%"
194
- height={height}
195
- borderStyle="round"
196
- borderColor={isFocused ? colors.accentCyan : colors.borderColor}
197
- padding={1}
198
- flexShrink={0}
199
- flexGrow={0}
200
- >
201
- <Text bold color={colors.textPrimary}>
202
- Projects ({projects.length})
203
- </Text>
204
- <Box flexDirection="column" flexGrow={1}>
205
- {projects.length === 0 ? (
206
- <Text color={colors.textTertiary}>No projects found</Text>
207
- ) : (
208
- <>
209
- {hasMoreAbove && (
210
- <Text color={colors.textTertiary}>↑ {startIndex} more above</Text>
211
- )}
212
- {visibleProjects.map((project, localIndex) => {
213
- const index = startIndex + localIndex;
214
- const isSelected = index === selectedIndex;
215
-
216
- // Check if this project has running scripts
217
- const projectRunning = runningProcesses.filter(
218
- (p: any) => p.projectPath === project.path
219
- );
220
- const hasRunningScripts = projectRunning.length > 0;
221
-
222
- return (
223
- <Text key={project.id} color={isSelected ? colors.accentCyan : colors.textPrimary} bold={isSelected}>
224
- {isSelected ? '▶ ' : ' '}
225
- {hasRunningScripts && <Text color={colors.accentGreen}>● </Text>}
226
- {truncateText(project.name, 30)}
227
- {hasRunningScripts && <Text color={colors.accentGreen}> ({projectRunning.length})</Text>}
228
- </Text>
229
- );
230
- })}
231
- {hasMoreBelow && (
232
- <Text color={colors.textTertiary}>↓ {projects.length - endIndex} more below</Text>
233
- )}
234
- </>
235
- )}
236
- </Box>
237
- </Box>
238
- );
239
- };
240
-
241
- interface ProjectDetailsProps {
242
- project: Project | null;
243
- runningProcesses: any[];
244
- isFocused: boolean;
245
- editingName: boolean;
246
- editingDescription: boolean;
247
- editingTags: boolean;
248
- editInput: string;
249
- allTags: string[];
250
- onTagRemove?: (tag: string) => void;
251
- height: number;
252
- scrollOffset: number;
253
- }
254
-
255
- const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
256
- project,
257
- runningProcesses,
258
- isFocused,
259
- editingName,
260
- editingDescription,
261
- editingTags,
262
- editInput,
263
- allTags,
264
- onTagRemove,
265
- height,
266
- scrollOffset,
267
- }) => {
268
- const { focus } = useFocus({ id: 'projectDetails' });
269
- const [scripts, setScripts] = useState<any>(null);
270
- const [ports, setPorts] = useState<any[]>([]);
271
- const [tests, setTests] = useState<any[]>([]);
272
-
273
- useEffect(() => {
274
- if (!project) {
275
- setScripts(null);
276
- setPorts([]);
277
- setTests([]);
278
- return;
279
- }
280
-
281
- // Load scripts
282
- try {
283
- const projectScripts = getProjectScripts(project.path);
284
- setScripts(projectScripts);
285
- } catch (error) {
286
- setScripts(null);
287
- }
288
-
289
- // Load ports
290
- try {
291
- const db = getDatabaseManager();
292
- const projectPorts = db.getProjectPorts(project.id);
293
- setPorts(projectPorts);
294
- } catch (error) {
295
- setPorts([]);
296
- }
297
-
298
- // Load tests
299
- try {
300
- const db = getDatabaseManager();
301
- const projectTests = db.getTestsByProject(project.id);
302
- setTests(projectTests);
303
- } catch (error) {
304
- setTests([]);
305
- }
306
- }, [project]);
307
-
308
- if (!project) {
309
- return (
310
- <Box
311
- flexDirection="column"
312
- flexGrow={1}
313
- borderStyle="round"
314
- borderColor={isFocused ? colors.accentCyan : colors.borderColor}
315
- padding={1}
316
- >
317
- <Text color={colors.textSecondary}>Select a project to view details</Text>
318
- </Box>
319
- );
320
- }
321
-
322
- const lastScanned = project.last_scanned
323
- ? new Date(project.last_scanned * 1000).toLocaleString()
324
- : 'Never';
325
-
326
- // Get running processes for this project
327
- const projectProcesses = runningProcesses.filter((p: any) => p.projectPath === project.path);
328
-
329
- // Count tests by framework
330
- const testsByFramework = tests.reduce((acc: any, test: any) => {
331
- const framework = test.framework || 'unknown';
332
- acc[framework] = (acc[framework] || 0) + 1;
333
- return acc;
334
- }, {});
335
-
336
- // Build content lines for virtual scrolling
337
- const contentLines: React.ReactNode[] = [];
338
-
339
- // Header section (always visible at top)
340
- contentLines.push(
341
- editingName ? (
342
- <Box key="edit-name">
343
- <Text color={colors.accentCyan}>Editing name: </Text>
344
- <Text color={colors.textPrimary}>{editInput}</Text>
345
- <Text color={colors.textSecondary}> (Press Enter to save, Esc to cancel)</Text>
346
- </Box>
347
- ) : (
348
- <Text key="name" bold color={colors.textPrimary}>
349
- {project.name}
350
- </Text>
351
- )
352
- );
353
-
354
- if (editingDescription) {
355
- contentLines.push(
356
- <Box key="edit-desc">
357
- <Text color={colors.accentCyan}>Editing description: </Text>
358
- <Text color={colors.textPrimary}>{editInput}</Text>
359
- <Text color={colors.textSecondary}> (Press Enter to save, Esc to cancel)</Text>
360
- </Box>
361
- );
362
- } else if (project.description) {
363
- contentLines.push(
364
- <Text key="desc" color={colors.textSecondary}>{truncateText(project.description, 100)}</Text>
365
- );
366
- }
367
-
368
- contentLines.push(
369
- <Text key="path" color={colors.textTertiary}>{truncateText(project.path, 100)}</Text>
370
- );
371
-
372
- // Tags (simplified to single line)
373
- if (project.tags && project.tags.length > 0) {
374
- const tagsText = project.tags.join(', ');
375
- contentLines.push(
376
- <Text key="tags">
377
- <Text color={colors.textSecondary}>Tags: </Text>
378
- <Text color={colors.accentPurple}>{truncateText(tagsText, 80)}</Text>
379
- </Text>
380
- );
381
- }
382
-
383
- if (editingTags) {
384
- contentLines.push(
385
- <Text key="edit-tags">
386
- <Text color={colors.accentCyan}>Add tag: </Text>
387
- <Text color={colors.textPrimary}>{editInput}</Text>
388
- </Text>
389
- );
390
- const suggestions = allTags.filter(t => t.toLowerCase().includes(editInput.toLowerCase()) && !project.tags?.includes(t)).slice(0, 3);
391
- if (editInput && suggestions.length > 0) {
392
- contentLines.push(
393
- <Text key="suggestions">
394
- <Text color={colors.textTertiary}>Suggestions: </Text>
395
- <Text color={colors.accentPurple}>{suggestions.join(', ')}</Text>
396
- </Text>
397
- );
398
- }
399
- }
400
-
401
- contentLines.push(<Text key="spacer1"> </Text>);
402
-
403
- // Stats
404
- contentLines.push(
405
- <Box key="stats">
406
- <Text>Tests: <Text color={colors.accentCyan}>{tests.length}</Text></Text>
407
- <Text> | </Text>
408
- <Text>Frameworks: <Text color={colors.accentCyan}>{Object.keys(testsByFramework).length}</Text></Text>
409
- <Text> | </Text>
410
- <Text>Ports: <Text color={colors.accentCyan}>{ports.length}</Text></Text>
411
- <Text> | </Text>
412
- <Text>Scripts: <Text color={colors.accentCyan}>{scripts?.scripts?.size || 0}</Text></Text>
413
- </Box>
414
- );
415
-
416
- contentLines.push(<Text key="spacer2"> </Text>);
417
-
418
- if (project.framework) {
419
- contentLines.push(
420
- <Text key="framework">
421
- Framework: <Text color={colors.accentCyan}>{project.framework}</Text>
422
- </Text>
423
- );
424
- }
425
-
426
- contentLines.push(
427
- <Text key="last-scanned">Last Scanned: {lastScanned}</Text>
428
- );
429
- contentLines.push(<Text key="spacer3"> </Text>);
430
-
431
- // Running Processes
432
- if (projectProcesses.length > 0) {
433
- contentLines.push(
434
- <Text key="proc-header" bold color={colors.accentGreen}>
435
- Running Processes ({projectProcesses.length}):
436
- </Text>
437
- );
438
- projectProcesses.forEach((process: any) => {
439
- const uptime = Math.floor((Date.now() - process.startedAt) / 1000);
440
- const minutes = Math.floor(uptime / 60);
441
- const seconds = uptime % 60;
442
- const uptimeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
443
-
444
- contentLines.push(
445
- <Text key={`proc-${process.pid}`}>
446
- {' '}
447
- <Text color={colors.accentGreen}>●</Text>
448
- {' '}
449
- <Text color={colors.textPrimary}>{process.scriptName}</Text>
450
- <Text color={colors.textSecondary}> (PID: {process.pid}, {uptimeStr})</Text>
451
- </Text>
452
- );
453
- });
454
- contentLines.push(<Text key="spacer4"> </Text>);
455
- }
456
-
457
- // Scripts
458
- if (scripts && scripts.scripts && scripts.scripts.size > 0) {
459
- contentLines.push(
460
- <Text key="scripts-header" bold>
461
- Available Scripts (<Text color={colors.accentCyan}>{scripts.scripts.size}</Text>):
462
- </Text>
463
- );
464
- Array.from(scripts.scripts.entries() as IterableIterator<[string, any]>).slice(0, 5).forEach(([name, script]) => {
465
- contentLines.push(
466
- <Text key={`script-${name}`}>
467
- {' '}
468
- <Text color={colors.accentGreen}>{name}</Text>
469
- {' - '}
470
- <Text color={colors.textSecondary}>{truncateText(script.command, 60)}</Text>
471
- </Text>
472
- );
473
- });
474
- if (scripts.scripts.size > 5) {
475
- contentLines.push(
476
- <Text key="scripts-more" color={colors.textTertiary}> ... and {scripts.scripts.size - 5} more</Text>
477
- );
478
- }
479
- contentLines.push(<Text key="spacer5"> </Text>);
480
- }
481
-
482
- // Ports
483
- if (ports.length > 0) {
484
- contentLines.push(
485
- <Text key="ports-header" bold>
486
- Detected Ports (<Text color={colors.accentCyan}>{ports.length}</Text>):
487
- </Text>
488
- );
489
- ports.slice(0, 5).forEach((port: any) => {
490
- contentLines.push(
491
- <Text key={`port-${port.id}`}>
492
- {' '}Port <Text color={colors.accentCyan}>{port.port}</Text>
493
- <Text color={colors.textSecondary}> - {truncateText(port.config_source, 50)}</Text>
494
- </Text>
495
- );
496
- });
497
- if (ports.length > 5) {
498
- contentLines.push(
499
- <Text key="ports-more" color={colors.textTertiary}> ... and {ports.length - 5} more</Text>
500
- );
501
- }
502
- contentLines.push(<Text key="spacer6"> </Text>);
503
- }
504
-
505
- // Test Files
506
- if (tests.length > 0) {
507
- contentLines.push(
508
- <Text key="tests-header" bold>
509
- Test Files (<Text color={colors.accentCyan}>{tests.length}</Text>):
510
- </Text>
511
- );
512
- Object.entries(testsByFramework).forEach(([framework, count]) => {
513
- contentLines.push(
514
- <Text key={`test-${framework}`}>
515
- {' '}
516
- <Text color={colors.accentPurple}>{framework}</Text>
517
- {': '}
518
- <Text color={colors.textSecondary}>{count as number}</Text>
519
- </Text>
520
- );
521
- });
522
- contentLines.push(<Text key="spacer7"> </Text>);
523
- }
524
-
525
- // Calculate visible range for virtual scrolling
526
- // Render enough items to fill the available space
527
- const startIndex = Math.max(0, scrollOffset);
528
- const hasMoreAbove = startIndex > 0;
529
- const paddingHeight = 2; // top + bottom padding
530
- // Reserve space for scroll indicators
531
- const reservedForIndicators = (hasMoreAbove ? 1 : 0) + 1;
532
- // Available space for content - be less conservative now that we have truncation
533
- const availableContentHeight = height - paddingHeight - reservedForIndicators;
534
- const visibleHeight = Math.max(5, availableContentHeight); // Render enough to fill space
535
- const endIndex = Math.min(contentLines.length, startIndex + visibleHeight);
536
- const visibleContent = contentLines.slice(startIndex, endIndex);
537
- const hasMoreBelow = endIndex < contentLines.length;
538
-
539
- return (
540
- <Box
541
- flexDirection="column"
542
- width="65%"
543
- height={height}
544
- borderStyle="round"
545
- borderColor={isFocused ? colors.accentCyan : colors.borderColor}
546
- padding={1}
547
- flexShrink={0}
548
- flexGrow={0}
549
- >
550
- <Box flexDirection="column" flexGrow={1}>
551
- {hasMoreAbove && (
552
- <Text color={colors.textTertiary}>↑ {startIndex} more above</Text>
553
- )}
554
- {visibleContent}
555
- {hasMoreBelow && (
556
- <Text color={colors.textTertiary}>↓ {contentLines.length - endIndex} more below</Text>
557
- )}
558
- </Box>
559
- </Box>
560
- );
561
- };
562
-
563
- interface StatusBarProps {
564
- focusedPanel: 'list' | 'details';
565
- selectedProject: Project | null;
566
- }
567
-
568
- const StatusBar: React.FC<StatusBarProps> = ({ focusedPanel, selectedProject }) => {
569
- if (focusedPanel === 'list') {
570
- return (
571
- <Box flexDirection="column">
572
- <Box>
573
- <Text color={colors.accentGreen}>● API</Text>
574
- <Text color={colors.textSecondary}> | </Text>
575
- <Text color={colors.textSecondary}>Focus: </Text>
576
- <Text color={colors.accentCyan}>Projects</Text>
577
- </Box>
578
- <Box>
579
- <Text bold>/</Text>
580
- <Text color={colors.textSecondary}> Search | </Text>
581
- <Text bold>↑↓/kj</Text>
582
- <Text color={colors.textSecondary}> Navigate | </Text>
583
- <Text bold>Tab/←→</Text>
584
- <Text color={colors.textSecondary}> Switch | </Text>
585
- <Text bold>s</Text>
586
- <Text color={colors.textSecondary}> Scan | </Text>
587
- <Text bold>?</Text>
588
- <Text color={colors.textSecondary}> Help | </Text>
589
- <Text bold>q</Text>
590
- <Text color={colors.textSecondary}> Quit</Text>
591
- </Box>
592
- </Box>
593
- );
594
- }
595
-
596
- // Details panel - show project-specific actions
597
- return (
598
- <Box flexDirection="column">
599
- <Box>
600
- <Text color={colors.accentGreen}>● API</Text>
601
- <Text color={colors.textSecondary}> | </Text>
602
- <Text color={colors.textSecondary}>Focus: </Text>
603
- <Text color={colors.accentCyan}>Details</Text>
604
- {selectedProject && (
605
- <>
606
- <Text color={colors.textSecondary}> | </Text>
607
- <Text color={colors.textPrimary}>{selectedProject.name}</Text>
608
- </>
609
- )}
610
- </Box>
611
- <Box>
612
- <Text bold>↑↓/kj</Text>
613
- <Text color={colors.textSecondary}> Scroll | </Text>
614
- <Text bold>e</Text>
615
- <Text color={colors.textSecondary}> Edit | </Text>
616
- <Text bold>t</Text>
617
- <Text color={colors.textSecondary}> Tags | </Text>
618
- <Text bold>o</Text>
619
- <Text color={colors.textSecondary}> Editor | </Text>
620
- <Text bold>f</Text>
621
- <Text color={colors.textSecondary}> Files | </Text>
622
- <Text bold>u</Text>
623
- <Text color={colors.textSecondary}> URLs | </Text>
624
- <Text bold>s</Text>
625
- <Text color={colors.textSecondary}> Scan | </Text>
626
- <Text bold>p</Text>
627
- <Text color={colors.textSecondary}> Ports | </Text>
628
- <Text bold>r</Text>
629
- <Text color={colors.textSecondary}> Scripts | </Text>
630
- <Text bold>x</Text>
631
- <Text color={colors.textSecondary}> Stop | </Text>
632
- <Text bold>d</Text>
633
- <Text color={colors.textSecondary}> Delete | </Text>
634
- <Text bold>Tab</Text>
635
- <Text color={colors.textSecondary}> Switch | </Text>
636
- <Text bold>?</Text>
637
- <Text color={colors.textSecondary}> Help | </Text>
638
- <Text bold>q</Text>
639
- <Text color={colors.textSecondary}> Quit</Text>
640
- </Box>
641
- </Box>
642
- );
643
- };
644
-
645
- // Simple fuzzy search function
646
- function fuzzyMatch(query: string, text: string): boolean {
647
- const queryLower = query.toLowerCase();
648
- const textLower = text.toLowerCase();
649
-
650
- if (queryLower === '') return true;
651
-
652
- let queryIndex = 0;
653
- for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
654
- if (textLower[i] === queryLower[queryIndex]) {
655
- queryIndex++;
656
- }
657
- }
658
-
659
- return queryIndex === queryLower.length;
660
- }
661
-
662
- const App: React.FC = () => {
663
- const { exit } = useApp();
664
- const { focusNext, focusPrevious } = useFocusManager();
665
- const [allProjects, setAllProjects] = useState<Project[]>([]);
666
- const [projects, setProjects] = useState<Project[]>([]);
667
- const [selectedIndex, setSelectedIndex] = useState(0);
668
- const [showHelp, setShowHelp] = useState(false);
669
- const [isLoading, setIsLoading] = useState(false);
670
- const [loadingMessage, setLoadingMessage] = useState('');
671
- const [error, setError] = useState<string | null>(null);
672
- const [runningProcesses, setRunningProcesses] = useState<any[]>([]);
673
- const [focusedPanel, setFocusedPanel] = useState<'list' | 'details'>('list');
674
-
675
- // Editing state
676
- const [editingName, setEditingName] = useState(false);
677
- const [editingDescription, setEditingDescription] = useState(false);
678
- const [editingTags, setEditingTags] = useState(false);
679
- const [editInput, setEditInput] = useState('');
680
- const [showUrls, setShowUrls] = useState(false);
681
- const [allTags, setAllTags] = useState<string[]>([]);
682
-
683
- // Search state
684
- const [showSearch, setShowSearch] = useState(false);
685
- const [searchQuery, setSearchQuery] = useState('');
686
- const [listScrollOffset, setListScrollOffset] = useState(0);
687
- const [detailsScrollOffset, setDetailsScrollOffset] = useState(0);
688
-
689
- // Get terminal dimensions
690
- const terminalHeight = process.stdout.rows || 24;
691
- const availableHeight = terminalHeight - 3; // Subtract status bar
692
-
693
- useEffect(() => {
694
- loadProjects();
695
- loadRunningProcesses();
696
- loadAllTags();
697
-
698
- // Refresh running processes every 5 seconds
699
- const interval = setInterval(() => {
700
- loadRunningProcesses();
701
- }, 5000);
702
-
703
- return () => clearInterval(interval);
704
- }, []);
705
-
706
- // Reset editing state and scroll when project changes
707
- useEffect(() => {
708
- setEditingName(false);
709
- setEditingDescription(false);
710
- setEditingTags(false);
711
- setEditInput('');
712
- setDetailsScrollOffset(0); // Reset scroll when switching projects
713
- }, [selectedIndex]);
714
-
715
- // Update scroll offset when selected index changes
716
- useEffect(() => {
717
- const visibleHeight = Math.max(1, availableHeight - 3);
718
- setListScrollOffset(prevOffset => {
719
- if (selectedIndex < prevOffset) {
720
- return Math.max(0, selectedIndex);
721
- } else if (selectedIndex >= prevOffset + visibleHeight) {
722
- return Math.max(0, selectedIndex - visibleHeight + 1);
723
- }
724
- return prevOffset;
725
- });
726
- }, [selectedIndex, availableHeight]);
727
-
728
- const loadAllTags = () => {
729
- try {
730
- const db = getDatabaseManager();
731
- const allProjects = getAllProjects();
732
- const tagsSet = new Set<string>();
733
- allProjects.forEach(project => {
734
- if (project.tags && Array.isArray(project.tags)) {
735
- project.tags.forEach(tag => tagsSet.add(tag));
736
- }
737
- });
738
- setAllTags(Array.from(tagsSet));
739
- } catch (error) {
740
- setAllTags([]);
741
- }
742
- };
743
-
744
- const loadProjects = () => {
745
- const loadedProjects = getAllProjects();
746
- setAllProjects(loadedProjects);
747
- filterProjects(loadedProjects, searchQuery);
748
- };
749
-
750
- const filterProjects = (projectsToFilter: Project[], query: string) => {
751
- if (!query.trim()) {
752
- setProjects(projectsToFilter);
753
- return;
754
- }
755
-
756
- const filtered = projectsToFilter.filter(project => {
757
- const nameMatch = fuzzyMatch(query, project.name);
758
- const descMatch = project.description ? fuzzyMatch(query, project.description) : false;
759
- const pathMatch = fuzzyMatch(query, project.path);
760
- const tagsMatch = project.tags?.some(tag => fuzzyMatch(query, tag)) || false;
761
-
762
- return nameMatch || descMatch || pathMatch || tagsMatch;
763
- });
764
-
765
- setProjects(filtered);
766
-
767
- // Adjust selected index if current selection is out of bounds
768
- if (selectedIndex >= filtered.length) {
769
- setSelectedIndex(Math.max(0, filtered.length - 1));
770
- }
771
- };
772
-
773
- const loadRunningProcesses = async () => {
774
- try {
775
- const processes = await getRunningProcessesClean();
776
- setRunningProcesses(processes);
777
- } catch (error) {
778
- setRunningProcesses([]);
779
- }
780
- };
781
-
782
- const selectedProject = projects.length > 0 ? projects[selectedIndex] : null;
783
-
784
- // Helper function to get editor command
785
- const getEditorCommand = (): { command: string; args: string[] } => {
786
- try {
787
- // Try to load settings from core
788
- const corePath = path.join(__dirname, '..', '..', 'core', 'dist', 'settings');
789
- const settingsPath = path.join(__dirname, '..', '..', '..', 'core', 'dist', 'settings');
790
- let settings: any;
791
-
792
- try {
793
- settings = require(corePath);
794
- } catch {
795
- try {
796
- settings = require(settingsPath);
797
- } catch {
798
- // Fallback to default
799
- return { command: 'code', args: [] };
800
- }
801
- }
802
-
803
- const editorSettings = settings.getEditorSettings();
804
- let command: string;
805
- const args: string[] = [];
806
-
807
- if (editorSettings.type === 'custom' && editorSettings.customPath) {
808
- command = editorSettings.customPath;
809
- } else {
810
- switch (editorSettings.type) {
811
- case 'vscode':
812
- command = 'code';
813
- break;
814
- case 'cursor':
815
- command = 'cursor';
816
- break;
817
- case 'windsurf':
818
- command = 'windsurf';
819
- break;
820
- case 'zed':
821
- command = 'zed';
822
- break;
823
- default:
824
- command = 'code';
825
- }
826
- }
827
-
828
- return { command, args };
829
- } catch (error) {
830
- return { command: 'code', args: [] };
831
- }
832
- };
833
-
834
- // Helper function to open project in editor
835
- const openInEditor = (projectPath: string) => {
836
- try {
837
- const { command, args } = getEditorCommand();
838
- spawn(command, [...args, projectPath], {
839
- detached: true,
840
- stdio: 'ignore',
841
- }).unref();
842
- } catch (error) {
843
- setError(`Failed to open editor: ${error instanceof Error ? error.message : String(error)}`);
844
- }
845
- };
846
-
847
- // Helper function to open project directory
848
- const openInFiles = (projectPath: string) => {
849
- try {
850
- let command: string;
851
- const args: string[] = [projectPath];
852
-
853
- if (os.platform() === 'darwin') {
854
- command = 'open';
855
- } else if (os.platform() === 'win32') {
856
- command = 'explorer';
857
- } else {
858
- command = 'xdg-open';
859
- }
860
-
861
- spawn(command, args, {
862
- detached: true,
863
- stdio: 'ignore',
864
- }).unref();
865
- } catch (error) {
866
- setError(`Failed to open file manager: ${error instanceof Error ? error.message : String(error)}`);
867
- }
868
- };
869
-
870
- // Helper function to get URLs from project
871
- const getProjectUrls = (project: Project): string[] => {
872
- const urls = new Set<string>();
873
-
874
- // Add URLs from running processes
875
- const projectProcesses = runningProcesses.filter((p: any) => p.projectPath === project.path);
876
- for (const process of projectProcesses) {
877
- if (process.detectedUrls && Array.isArray(process.detectedUrls)) {
878
- for (const url of process.detectedUrls) {
879
- urls.add(url);
880
- }
881
- }
882
- }
883
-
884
- // Add URLs from detected ports
885
- try {
886
- const db = getDatabaseManager();
887
- const projectPorts = db.getProjectPorts(project.id);
888
- for (const portInfo of projectPorts) {
889
- const url = `http://localhost:${portInfo.port}`;
890
- urls.add(url);
891
- }
892
- } catch (error) {
893
- // Ignore
894
- }
895
-
896
- return Array.from(urls).sort();
897
- };
898
-
899
- useInput((input: string, key: any) => {
900
- // Handle search mode
901
- if (showSearch) {
902
- if (key.escape) {
903
- setShowSearch(false);
904
- setSearchQuery('');
905
- filterProjects(allProjects, '');
906
- return;
907
- }
908
-
909
- if (key.return) {
910
- setShowSearch(false);
911
- return;
912
- }
913
-
914
- if (key.backspace || key.delete) {
915
- const newQuery = searchQuery.slice(0, -1);
916
- setSearchQuery(newQuery);
917
- filterProjects(allProjects, newQuery);
918
- return;
919
- }
920
-
921
- if (input && input.length === 1 && !key.ctrl && !key.meta) {
922
- const newQuery = searchQuery + input;
923
- setSearchQuery(newQuery);
924
- filterProjects(allProjects, newQuery);
925
- return;
926
- }
927
-
928
- return;
929
- }
930
-
931
- // Don't process input if modal is showing
932
- if (showHelp || isLoading || error || showUrls) {
933
- // Handle URLs modal
934
- if (showUrls && (key.escape || key.return || input === 'q' || input === 'u')) {
935
- setShowUrls(false);
936
- return;
937
- }
938
- return;
939
- }
940
-
941
- // Search shortcut
942
- if (input === '/') {
943
- setShowSearch(true);
944
- setSearchQuery('');
945
- return;
946
- }
947
-
948
- // Handle editing modes
949
- if (editingName || editingDescription || editingTags) {
950
- if (key.escape) {
951
- setEditingName(false);
952
- setEditingDescription(false);
953
- setEditingTags(false);
954
- setEditInput('');
955
- return;
956
- }
957
-
958
- if (key.return) {
959
- // Save changes
960
- if (selectedProject) {
961
- if (editingName && editInput.trim()) {
962
- try {
963
- const db = getDatabaseManager();
964
- db.updateProjectName(selectedProject.id, editInput.trim());
965
- loadProjects();
966
- setEditingName(false);
967
- setEditInput('');
968
- } catch (err) {
969
- setError(err instanceof Error ? err.message : String(err));
970
- }
971
- } else if (editingDescription) {
972
- try {
973
- const db = getDatabaseManager();
974
- db.updateProject(selectedProject.id, { description: editInput.trim() || null });
975
- loadProjects();
976
- setEditingDescription(false);
977
- setEditInput('');
978
- } catch (err) {
979
- setError(err instanceof Error ? err.message : String(err));
980
- }
981
- } else if (editingTags) {
982
- // Handle tag input
983
- const newTag = editInput.trim();
984
- if (newTag) {
985
- try {
986
- const db = getDatabaseManager();
987
- const currentTags = selectedProject.tags || [];
988
- if (!currentTags.includes(newTag)) {
989
- db.updateProject(selectedProject.id, { tags: [...currentTags, newTag] });
990
- loadProjects();
991
- loadAllTags();
992
- }
993
- setEditInput('');
994
- } catch (err) {
995
- setError(err instanceof Error ? err.message : String(err));
996
- }
997
- }
998
- }
999
- }
1000
- return;
1001
- }
1002
-
1003
- // Handle backspace
1004
- if (key.backspace || key.delete) {
1005
- setEditInput(prev => prev.slice(0, -1));
1006
- return;
1007
- }
1008
-
1009
- // Handle regular input
1010
- if (input && input.length === 1) {
1011
- setEditInput(prev => prev + input);
1012
- return;
1013
- }
1014
-
1015
- return;
1016
- }
1017
-
1018
- // Quit
1019
- if (input === 'q' || key.escape) {
1020
- exit();
1021
- return;
1022
- }
1023
-
1024
- // Help
1025
- if (input === '?') {
1026
- setShowHelp(true);
1027
- return;
1028
- }
1029
-
1030
- // Switch panels with Tab or Left/Right arrows
1031
- if (key.tab || key.leftArrow || key.rightArrow) {
1032
- setFocusedPanel((prev) => prev === 'list' ? 'details' : 'list');
1033
- return;
1034
- }
1035
-
1036
- // Navigation (only when focused on list)
1037
- if (focusedPanel === 'list') {
1038
- if (key.upArrow || input === 'k') {
1039
- setSelectedIndex((prev) => {
1040
- const newIndex = Math.max(0, prev - 1);
1041
- // Update scroll offset
1042
- const visibleHeight = Math.max(1, availableHeight - 3);
1043
- if (newIndex < listScrollOffset) {
1044
- setListScrollOffset(Math.max(0, newIndex));
1045
- }
1046
- return newIndex;
1047
- });
1048
- return;
1049
- }
1050
-
1051
- if (key.downArrow || input === 'j') {
1052
- setSelectedIndex((prev) => {
1053
- const newIndex = Math.min(projects.length - 1, prev + 1);
1054
- // Update scroll offset
1055
- const visibleHeight = Math.max(1, availableHeight - 3);
1056
- if (newIndex >= listScrollOffset + visibleHeight) {
1057
- setListScrollOffset(Math.max(0, newIndex - visibleHeight + 1));
1058
- }
1059
- return newIndex;
1060
- });
1061
- return;
1062
- }
1063
- }
1064
-
1065
- // Details panel actions
1066
- if (focusedPanel === 'details' && selectedProject) {
1067
- // Scroll details panel
1068
- if (key.upArrow || input === 'k') {
1069
- setDetailsScrollOffset(prev => Math.max(0, prev - 1));
1070
- return;
1071
- }
1072
-
1073
- if (key.downArrow || input === 'j') {
1074
- const visibleHeight = Math.max(1, availableHeight - 3);
1075
- // Estimate content height (rough calculation)
1076
- setDetailsScrollOffset(prev => prev + 1);
1077
- return;
1078
- }
1079
-
1080
- // Edit name
1081
- if (input === 'e') {
1082
- setEditingName(true);
1083
- setEditingDescription(false);
1084
- setEditingTags(false);
1085
- setEditInput(selectedProject.name);
1086
- return;
1087
- }
1088
-
1089
- // Edit tags
1090
- if (input === 't') {
1091
- setEditingTags(true);
1092
- setEditingName(false);
1093
- setEditingDescription(false);
1094
- setEditInput('');
1095
- return;
1096
- }
1097
-
1098
- // Open in editor
1099
- if (input === 'o') {
1100
- openInEditor(selectedProject.path);
1101
- return;
1102
- }
1103
-
1104
- // Open in file manager
1105
- if (input === 'f') {
1106
- openInFiles(selectedProject.path);
1107
- return;
1108
- }
1109
-
1110
- // Show URLs
1111
- if (input === 'u') {
1112
- setShowUrls(true);
1113
- return;
1114
- }
1115
-
1116
- // Delete project
1117
- if (input === 'd') {
1118
- setIsLoading(true);
1119
- setLoadingMessage(`Deleting ${selectedProject.name}...`);
1120
- setTimeout(async () => {
1121
- try {
1122
- const db = getDatabaseManager();
1123
- db.removeProject(selectedProject.id);
1124
- loadProjects();
1125
- if (selectedIndex >= projects.length - 1) {
1126
- setSelectedIndex(Math.max(0, projects.length - 2));
1127
- }
1128
- setIsLoading(false);
1129
- } catch (err) {
1130
- setIsLoading(false);
1131
- setError(err instanceof Error ? err.message : String(err));
1132
- }
1133
- }, 100);
1134
- return;
1135
- }
1136
- }
1137
-
1138
- // Scan project
1139
- if (input === 's' && selectedProject) {
1140
- setIsLoading(true);
1141
- setLoadingMessage(`Scanning ${selectedProject.name}...`);
1142
-
1143
- setTimeout(async () => {
1144
- try {
1145
- await scanProject(selectedProject.id);
1146
- loadProjects();
1147
- setIsLoading(false);
1148
- } catch (err) {
1149
- setIsLoading(false);
1150
- setError(err instanceof Error ? err.message : String(err));
1151
- }
1152
- }, 100);
1153
- return;
1154
- }
1155
-
1156
- // Scan ports
1157
- if (input === 'p' && selectedProject) {
1158
- setIsLoading(true);
1159
- setLoadingMessage(`Scanning ports for ${selectedProject.name}...`);
1160
-
1161
- setTimeout(async () => {
1162
- try {
1163
- // Import port scanner dynamically
1164
- const { scanProjectPorts } = await import('../../cli/src/port-scanner');
1165
- await scanProjectPorts(selectedProject.id);
1166
- loadProjects();
1167
- setIsLoading(false);
1168
- } catch (err) {
1169
- setIsLoading(false);
1170
- setError(err instanceof Error ? err.message : String(err));
1171
- }
1172
- }, 100);
1173
- return;
1174
- }
1175
-
1176
- // Run script
1177
- if (input === 'r' && selectedProject) {
1178
- setIsLoading(true);
1179
- setLoadingMessage('Select script to run...');
1180
-
1181
- setTimeout(async () => {
1182
- try {
1183
- const projectScripts = getProjectScripts(selectedProject.path);
1184
- if (projectScripts.scripts.size === 0) {
1185
- setIsLoading(false);
1186
- setError(`No scripts found in ${selectedProject.name}`);
1187
- return;
1188
- }
1189
-
1190
- setIsLoading(false);
1191
-
1192
- // For now, just show error to select a script
1193
- const scriptList = Array.from(projectScripts.scripts.keys()).join(', ');
1194
- setError(`Use CLI to run scripts: prx run ${selectedProject.name} <script>\nAvailable: ${scriptList}`);
1195
- } catch (err) {
1196
- setIsLoading(false);
1197
- setError(err instanceof Error ? err.message : String(err));
1198
- }
1199
- }, 100);
1200
- return;
1201
- }
1202
-
1203
- // Stop all scripts for project
1204
- if (input === 'x' && selectedProject) {
1205
- setIsLoading(true);
1206
- setLoadingMessage(`Stopping scripts for ${selectedProject.name}...`);
1207
-
1208
- setTimeout(async () => {
1209
- try {
1210
- const projectProcesses = runningProcesses.filter((p: any) => p.projectPath === selectedProject.path);
1211
- if (projectProcesses.length === 0) {
1212
- setIsLoading(false);
1213
- setError(`No running scripts for ${selectedProject.name}`);
1214
- return;
1215
- }
1216
-
1217
- for (const proc of projectProcesses) {
1218
- await stopScript(proc.pid);
1219
- }
1220
-
1221
- await loadRunningProcesses();
1222
- setIsLoading(false);
1223
- } catch (err) {
1224
- setIsLoading(false);
1225
- setError(err instanceof Error ? err.message : String(err));
1226
- }
1227
- }, 100);
1228
- return;
1229
- }
1230
-
1231
- });
1232
-
1233
- if (showHelp) {
1234
- return (
1235
- <Box flexDirection="column" padding={1}>
1236
- <HelpModal onClose={() => setShowHelp(false)} />
1237
- </Box>
1238
- );
1239
- }
1240
-
1241
- if (isLoading) {
1242
- return (
1243
- <Box flexDirection="column" padding={1}>
1244
- <LoadingModal message={loadingMessage} />
1245
- </Box>
1246
- );
1247
- }
1248
-
1249
- if (error) {
1250
- return (
1251
- <Box flexDirection="column" padding={1}>
1252
- <ErrorModal message={error} onClose={() => setError(null)} />
1253
- </Box>
1254
- );
1255
- }
1256
-
1257
- if (showSearch) {
1258
- return (
1259
- <Box flexDirection="column" padding={1}>
1260
- <Box
1261
- flexDirection="column"
1262
- borderStyle="round"
1263
- borderColor={colors.accentCyan}
1264
- padding={1}
1265
- width={60}
1266
- >
1267
- <Text bold color={colors.accentCyan}>
1268
- Search Projects
1269
- </Text>
1270
- <Text> </Text>
1271
- <Text color={colors.textPrimary}>
1272
- /{searchQuery}
1273
- <Text color={colors.textTertiary}>_</Text>
1274
- </Text>
1275
- <Text> </Text>
1276
- {projects.length === 0 ? (
1277
- <Text color={colors.textTertiary}>No projects match "{searchQuery}"</Text>
1278
- ) : (
1279
- <Text color={colors.textSecondary}>
1280
- Found {projects.length} project{projects.length !== 1 ? 's' : ''}
1281
- </Text>
1282
- )}
1283
- <Text> </Text>
1284
- <Text color={colors.textSecondary}>Press Enter to confirm, Esc to cancel</Text>
1285
- </Box>
1286
- </Box>
1287
- );
1288
- }
1289
-
1290
- if (showUrls && selectedProject) {
1291
- const urls = getProjectUrls(selectedProject);
1292
- return (
1293
- <Box flexDirection="column" padding={1}>
1294
- <Box
1295
- flexDirection="column"
1296
- borderStyle="round"
1297
- borderColor={colors.accentCyan}
1298
- padding={1}
1299
- width={70}
1300
- >
1301
- <Text bold color={colors.accentCyan}>
1302
- URLs for {selectedProject.name}
1303
- </Text>
1304
- <Text> </Text>
1305
- {urls.length === 0 ? (
1306
- <Text color={colors.textTertiary}>No URLs detected</Text>
1307
- ) : (
1308
- urls.map((url, idx) => (
1309
- <Text key={idx} color={colors.textPrimary}>
1310
- {idx + 1}. {url}
1311
- </Text>
1312
- ))
1313
- )}
1314
- <Text> </Text>
1315
- <Text color={colors.textSecondary}>Press Esc or u to close...</Text>
1316
- </Box>
1317
- </Box>
1318
- );
1319
- }
1320
-
1321
- const handleTagRemove = (tag: string) => {
1322
- if (selectedProject) {
1323
- try {
1324
- const db = getDatabaseManager();
1325
- const currentTags = selectedProject.tags || [];
1326
- db.updateProject(selectedProject.id, { tags: currentTags.filter(t => t !== tag) });
1327
- loadProjects();
1328
- loadAllTags();
1329
- } catch (err) {
1330
- setError(err instanceof Error ? err.message : String(err));
1331
- }
1332
- }
1333
- };
1334
-
1335
- return (
1336
- <Box flexDirection="column" height={terminalHeight}>
1337
- <Box flexDirection="row" height={availableHeight} flexGrow={0} flexShrink={0}>
1338
- <ProjectListComponent
1339
- projects={projects}
1340
- selectedIndex={selectedIndex}
1341
- runningProcesses={runningProcesses}
1342
- isFocused={focusedPanel === 'list'}
1343
- height={availableHeight}
1344
- scrollOffset={listScrollOffset}
1345
- />
1346
- <Box width={1} />
1347
- <ProjectDetailsComponent
1348
- project={selectedProject}
1349
- runningProcesses={runningProcesses}
1350
- isFocused={focusedPanel === 'details'}
1351
- editingName={editingName}
1352
- editingDescription={editingDescription}
1353
- editingTags={editingTags}
1354
- editInput={editInput}
1355
- allTags={allTags}
1356
- onTagRemove={handleTagRemove}
1357
- height={availableHeight}
1358
- scrollOffset={detailsScrollOffset}
1359
- />
1360
- </Box>
1361
-
1362
- <Box paddingX={1} borderStyle="single" borderColor={colors.borderColor} flexShrink={0} height={3}>
1363
- <StatusBar focusedPanel={focusedPanel} selectedProject={selectedProject} />
1364
- </Box>
1365
- </Box>
1366
- );
1367
- };
1368
-
1369
- // Render the app
1370
- render(<App />);