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.
- package/coverage/core-bridge.ts.html +24 -3
- package/coverage/index.html +34 -19
- package/coverage/lcov-report/core-bridge.ts.html +24 -3
- package/coverage/lcov-report/index.html +34 -19
- package/coverage/lcov-report/port-extractor.ts.html +1 -1
- package/coverage/lcov-report/port-scanner.ts.html +3 -3
- package/coverage/lcov-report/port-utils.ts.html +1 -1
- package/coverage/lcov-report/script-runner.ts.html +302 -11
- package/coverage/lcov-report/test-parser.ts.html +799 -0
- package/coverage/lcov.info +270 -49
- package/coverage/port-extractor.ts.html +1 -1
- package/coverage/port-scanner.ts.html +3 -3
- package/coverage/port-utils.ts.html +1 -1
- package/coverage/script-runner.ts.html +302 -11
- package/coverage/test-parser.ts.html +799 -0
- package/dist/api/__tests__/routes.test.js +1 -0
- package/dist/api/__tests__/routes.test.js.map +1 -1
- package/dist/api/__tests__/scanner.test.js +1 -0
- package/dist/api/__tests__/scanner.test.js.map +1 -1
- package/dist/api/package.json +4 -0
- package/dist/api/routes/backup.d.ts +2 -1
- package/dist/api/routes/backup.d.ts.map +1 -1
- package/dist/api/routes/backup.js.map +1 -1
- package/dist/api/routes/index.d.ts +2 -1
- package/dist/api/routes/index.d.ts.map +1 -1
- package/dist/api/routes/index.js.map +1 -1
- package/dist/api/routes/projects.d.ts +2 -1
- package/dist/api/routes/projects.d.ts.map +1 -1
- package/dist/api/routes/projects.js.map +1 -1
- package/dist/api/routes/settings.d.ts +2 -1
- package/dist/api/routes/settings.d.ts.map +1 -1
- package/dist/api/routes/settings.js +21 -1
- package/dist/api/routes/settings.js.map +1 -1
- package/dist/api/routes/workspaces.d.ts +2 -1
- package/dist/api/routes/workspaces.d.ts.map +1 -1
- package/dist/api/routes/workspaces.js.map +1 -1
- package/dist/core/__tests__/index.test.js +1 -0
- package/dist/core/__tests__/scanner.test.js +1 -0
- package/dist/core/__tests__/settings.test.js +1 -0
- package/dist/core-bridge.js +5 -1
- package/dist/electron/core/__tests__/index.test.js +1 -0
- package/dist/electron/core/__tests__/scanner.test.js +1 -0
- package/dist/electron/core/__tests__/settings.test.js +1 -0
- package/dist/electron/core.js +5 -1
- package/dist/electron/main.js +113 -99
- package/dist/electron/port-scanner.js +1 -1
- package/dist/electron/renderer/assets/index-BjZn_mEF.js +66 -0
- package/dist/electron/renderer/assets/index-CZmDxbJO.js +66 -0
- package/dist/electron/renderer/assets/{index-CWxXs5M7.css → index-DfocdjIj.css} +1 -1
- package/dist/electron/renderer/index.html +2 -2
- package/dist/index.js +19 -19
- package/dist/port-scanner.js +1 -1
- package/dist/prxi.d.ts +1 -0
- package/dist/prxi.js +1106 -0
- package/dist/prxi.tsx +6 -6
- package/jest.config.js +8 -0
- package/package.json +8 -4
- package/dist/api/routes/mcp.d.ts +0 -3
- package/dist/api/routes/mcp.d.ts.map +0 -1
- package/dist/api/routes/mcp.js +0 -147
- package/dist/api/routes/mcp.js.map +0 -1
- package/dist/electron/renderer/assets/index-59AhiV_K.css +0 -1
- package/dist/electron/renderer/assets/index-A04svynq.js +0 -62
- package/dist/electron/renderer/assets/index-B-etDnj2.js +0 -64
- package/dist/electron/renderer/assets/index-BGodNljq.js +0 -62
- package/dist/electron/renderer/assets/index-Bx18Cyic.js +0 -64
- package/dist/electron/renderer/assets/index-ByBOaxqv.js +0 -62
- package/dist/electron/renderer/assets/index-ByHY-x-j.js +0 -62
- package/dist/electron/renderer/assets/index-C1SRt6Jx.js +0 -62
- package/dist/electron/renderer/assets/index-C8f5yNYe.js +0 -64
- package/dist/electron/renderer/assets/index-C9Fo49a8.js +0 -61
- package/dist/electron/renderer/assets/index-CGx7K7jh.js +0 -62
- package/dist/electron/renderer/assets/index-CIZ3Wl6c.css +0 -1
- package/dist/electron/renderer/assets/index-CJbsU9y8.css +0 -1
- package/dist/electron/renderer/assets/index-CJrLunKK.js +0 -62
- package/dist/electron/renderer/assets/index-CQTleudf.css +0 -1
- package/dist/electron/renderer/assets/index-CQcilqlv.js +0 -62
- package/dist/electron/renderer/assets/index-CS-85xbL.css +0 -1
- package/dist/electron/renderer/assets/index-CYph0WPA.js +0 -62
- package/dist/electron/renderer/assets/index-C_WSLD6y.css +0 -1
- package/dist/electron/renderer/assets/index-CgB-tTpV.js +0 -62
- package/dist/electron/renderer/assets/index-ChoTzPLo.css +0 -1
- package/dist/electron/renderer/assets/index-CopVNRnR.js +0 -64
- package/dist/electron/renderer/assets/index-CtXtIMer.js +0 -64
- package/dist/electron/renderer/assets/index-D1jmaGv5.css +0 -1
- package/dist/electron/renderer/assets/index-D2AOB6Er.js +0 -62
- package/dist/electron/renderer/assets/index-DAfjuYKX.js +0 -61
- package/dist/electron/renderer/assets/index-DEOOHPEi.css +0 -1
- package/dist/electron/renderer/assets/index-DTtg6XrF.css +0 -1
- package/dist/electron/renderer/assets/index-DUvcepWm.js +0 -64
- package/dist/electron/renderer/assets/index-DVWDlM1D.js +0 -62
- package/dist/electron/renderer/assets/index-DWe2TQFv.css +0 -1
- package/dist/electron/renderer/assets/index-DZzB20Xf.css +0 -1
- package/dist/electron/renderer/assets/index-Dk0EQt0u.css +0 -1
- package/dist/electron/renderer/assets/index-DknLdADV.js +0 -63
- package/dist/electron/renderer/assets/index-DocuD8Lk.js +0 -64
- package/dist/electron/renderer/assets/index-DwRy5FqP.js +0 -62
- package/dist/electron/renderer/assets/index-DyU-xfd8.css +0 -1
- package/dist/electron/renderer/assets/index-GwC-JVUy.css +0 -1
- package/dist/electron/renderer/assets/index-JXrtTB1F.js +0 -63
- package/dist/electron/renderer/assets/index-Ocrdv8Lb.css +0 -1
- package/dist/electron/renderer/assets/index-R-HsWJ0K.js +0 -62
- package/dist/electron/renderer/assets/index-Ytah0wbZ.js +0 -62
- package/dist/electron/renderer/assets/index-ZVyXUshO.css +0 -1
- package/dist/electron/renderer/assets/index-Z_8dJn3i.js +0 -62
- package/dist/electron/renderer/assets/index-fehviker.js +0 -63
- package/dist/electron/renderer/assets/index-nts9ST-M.js +0 -62
- package/dist/electron/renderer/assets/index-q8NVIH3g.css +0 -1
- package/dist/electron/renderer/assets/index-thUWIXon.js +0 -62
- package/dist/electron/renderer/assets/index-tuQmrwcm.css +0 -1
- 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));
|