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