projax 3.3.58 → 3.3.63
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/README.md +10 -1
- package/dist/electron/preload.d.ts +1 -0
- package/dist/electron/renderer/assets/index-CmtZriN5.js +66 -0
- package/dist/electron/renderer/index.html +1 -1
- package/dist/electron/script-runner.js +52 -20
- package/dist/index.js +14 -10
- package/dist/prxi.js +877 -109
- package/dist/prxi.tsx +1249 -177
- package/dist/script-runner.js +52 -20
- package/package.json +1 -1
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/core-bridge.ts.html +0 -292
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -191
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/core-bridge.ts.html +0 -292
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -191
- package/coverage/lcov-report/port-extractor.ts.html +0 -1174
- package/coverage/lcov-report/port-scanner.ts.html +0 -301
- package/coverage/lcov-report/port-utils.ts.html +0 -670
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/script-runner.ts.html +0 -3346
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/test-parser.ts.html +0 -799
- package/coverage/lcov.info +0 -1338
- package/coverage/port-extractor.ts.html +0 -1174
- package/coverage/port-scanner.ts.html +0 -301
- package/coverage/port-utils.ts.html +0 -670
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/script-runner.ts.html +0 -3346
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/test-parser.ts.html +0 -799
- package/dist/__tests__/core-bridge.test.d.ts +0 -1
- package/dist/__tests__/core-bridge.test.js +0 -135
- package/dist/__tests__/port-extractor.test.d.ts +0 -1
- package/dist/__tests__/port-extractor.test.js +0 -407
- package/dist/__tests__/port-scanner.test.d.ts +0 -1
- package/dist/__tests__/port-scanner.test.js +0 -170
- package/dist/__tests__/port-utils.test.d.ts +0 -1
- package/dist/__tests__/port-utils.test.js +0 -127
- package/dist/__tests__/script-runner.test.d.ts +0 -1
- package/dist/__tests__/script-runner.test.js +0 -491
- package/dist/__tests__/test-parser.test.d.ts +0 -1
- package/dist/__tests__/test-parser.test.js +0 -276
- package/dist/api/__tests__/database.test.d.ts +0 -2
- package/dist/api/__tests__/database.test.d.ts.map +0 -1
- package/dist/api/__tests__/database.test.js +0 -485
- package/dist/api/__tests__/database.test.js.map +0 -1
- package/dist/api/__tests__/routes.test.d.ts +0 -2
- package/dist/api/__tests__/routes.test.d.ts.map +0 -1
- package/dist/api/__tests__/routes.test.js +0 -484
- package/dist/api/__tests__/routes.test.js.map +0 -1
- package/dist/api/__tests__/scanner.test.d.ts +0 -2
- package/dist/api/__tests__/scanner.test.d.ts.map +0 -1
- package/dist/api/__tests__/scanner.test.js +0 -403
- package/dist/api/__tests__/scanner.test.js.map +0 -1
- package/dist/core/__tests__/database.test.d.ts +0 -1
- package/dist/core/__tests__/database.test.js +0 -557
- package/dist/core/__tests__/detector.test.d.ts +0 -1
- package/dist/core/__tests__/detector.test.js +0 -375
- package/dist/core/__tests__/index.test.d.ts +0 -1
- package/dist/core/__tests__/index.test.js +0 -469
- package/dist/core/__tests__/scanner.test.d.ts +0 -1
- package/dist/core/__tests__/scanner.test.js +0 -406
- package/dist/core/__tests__/settings.test.d.ts +0 -1
- package/dist/core/__tests__/settings.test.js +0 -280
- package/dist/electron/core/__tests__/database.test.d.ts +0 -1
- package/dist/electron/core/__tests__/database.test.js +0 -557
- package/dist/electron/core/__tests__/detector.test.d.ts +0 -1
- package/dist/electron/core/__tests__/detector.test.js +0 -375
- package/dist/electron/core/__tests__/index.test.d.ts +0 -1
- package/dist/electron/core/__tests__/index.test.js +0 -469
- package/dist/electron/core/__tests__/scanner.test.d.ts +0 -1
- package/dist/electron/core/__tests__/scanner.test.js +0 -406
- package/dist/electron/core/__tests__/settings.test.d.ts +0 -1
- package/dist/electron/core/__tests__/settings.test.js +0 -280
- package/jest.config.js +0 -26
package/dist/prxi.js
CHANGED
|
@@ -35,11 +35,24 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
const react_1 = __importStar(require("react"));
|
|
37
37
|
const ink_1 = require("ink");
|
|
38
|
+
// Handle EPIPE errors gracefully to prevent crashes when output streams close
|
|
39
|
+
process.stdout.on('error', (err) => {
|
|
40
|
+
if (err.code === 'EPIPE') {
|
|
41
|
+
// Output stream closed, exit gracefully
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
process.stderr.on('error', (err) => {
|
|
46
|
+
if (err.code === 'EPIPE') {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
38
50
|
const core_bridge_1 = require("./core-bridge");
|
|
39
51
|
const script_runner_1 = require("./script-runner");
|
|
40
52
|
const child_process_1 = require("child_process");
|
|
41
53
|
const path = __importStar(require("path"));
|
|
42
54
|
const os = __importStar(require("os"));
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
43
56
|
// Color scheme matching desktop app
|
|
44
57
|
const colors = {
|
|
45
58
|
bgPrimary: '#0d1117',
|
|
@@ -74,26 +87,51 @@ function truncateText(text, maxLength) {
|
|
|
74
87
|
return text;
|
|
75
88
|
return text.substring(0, maxLength - 3) + '...';
|
|
76
89
|
}
|
|
90
|
+
const FILTER_TYPES = ['all', 'name', 'path', 'ports', 'tags', 'running'];
|
|
91
|
+
const SORT_TYPES = ['name-asc', 'name-desc', 'recent', 'oldest', 'running'];
|
|
92
|
+
const FILTER_LABELS = {
|
|
93
|
+
'all': 'All',
|
|
94
|
+
'name': 'Name',
|
|
95
|
+
'path': 'Path',
|
|
96
|
+
'ports': 'Ports',
|
|
97
|
+
'tags': 'Tags',
|
|
98
|
+
'running': 'Running',
|
|
99
|
+
};
|
|
100
|
+
const SORT_LABELS = {
|
|
101
|
+
'name-asc': 'Name A-Z',
|
|
102
|
+
'name-desc': 'Name Z-A',
|
|
103
|
+
'recent': 'Recently Scanned',
|
|
104
|
+
'oldest': 'Oldest First',
|
|
105
|
+
'running': 'Running First',
|
|
106
|
+
};
|
|
77
107
|
const HelpModal = ({ onClose }) => {
|
|
78
108
|
(0, ink_1.useInput)((input, key) => {
|
|
79
109
|
if (input === 'q' || key.escape || key.return) {
|
|
80
110
|
onClose();
|
|
81
111
|
}
|
|
82
112
|
});
|
|
83
|
-
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentCyan, padding: 1, width:
|
|
113
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentCyan, padding: 1, width: 75 },
|
|
84
114
|
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, "PROJAX Terminal UI - Help"),
|
|
85
115
|
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, "
|
|
88
|
-
react_1.default.createElement(ink_1.Text, null, "
|
|
89
|
-
react_1.default.createElement(ink_1.Text, null, "
|
|
116
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "View Navigation:"),
|
|
117
|
+
react_1.default.createElement(ink_1.Text, null, " 1 Projects view"),
|
|
118
|
+
react_1.default.createElement(ink_1.Text, null, " 2 Workspaces view"),
|
|
119
|
+
react_1.default.createElement(ink_1.Text, null, " 3 Global processes view"),
|
|
120
|
+
react_1.default.createElement(ink_1.Text, null, " 4 Settings"),
|
|
121
|
+
react_1.default.createElement(ink_1.Text, null, " T Toggle terminal output panel"),
|
|
90
122
|
react_1.default.createElement(ink_1.Text, null, " "),
|
|
91
|
-
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "
|
|
123
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Projects View - Navigation:"),
|
|
124
|
+
react_1.default.createElement(ink_1.Text, null, " \u2191/k Move up in list"),
|
|
125
|
+
react_1.default.createElement(ink_1.Text, null, " \u2193/j Move down in list"),
|
|
126
|
+
react_1.default.createElement(ink_1.Text, null, " Tab/\u2190\u2192 Switch between list and details"),
|
|
92
127
|
react_1.default.createElement(ink_1.Text, null, " / Search projects (fuzzy search)"),
|
|
93
|
-
react_1.default.createElement(ink_1.Text, null, "
|
|
128
|
+
react_1.default.createElement(ink_1.Text, null, " F Cycle filter type (all/name/path/ports/tags/running)"),
|
|
129
|
+
react_1.default.createElement(ink_1.Text, null, " S Cycle sort order (name/recent/oldest/running)"),
|
|
130
|
+
react_1.default.createElement(ink_1.Text, null, " g Refresh git branches"),
|
|
131
|
+
react_1.default.createElement(ink_1.Text, null, " R Full refresh (projects + branches + processes)"),
|
|
94
132
|
react_1.default.createElement(ink_1.Text, null, " "),
|
|
95
|
-
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "
|
|
96
|
-
react_1.default.createElement(ink_1.Text, null, "
|
|
133
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Projects View - Actions:"),
|
|
134
|
+
react_1.default.createElement(ink_1.Text, null, " a Add new project"),
|
|
97
135
|
react_1.default.createElement(ink_1.Text, null, " e Edit project name"),
|
|
98
136
|
react_1.default.createElement(ink_1.Text, null, " t Add/edit tags"),
|
|
99
137
|
react_1.default.createElement(ink_1.Text, null, " o Open project in editor"),
|
|
@@ -101,16 +139,12 @@ const HelpModal = ({ onClose }) => {
|
|
|
101
139
|
react_1.default.createElement(ink_1.Text, null, " u Show detected URLs"),
|
|
102
140
|
react_1.default.createElement(ink_1.Text, null, " s Scan project for tests"),
|
|
103
141
|
react_1.default.createElement(ink_1.Text, null, " p Scan ports for project"),
|
|
104
|
-
react_1.default.createElement(ink_1.Text, null, " r
|
|
142
|
+
react_1.default.createElement(ink_1.Text, null, " r Run scripts (select from list)"),
|
|
105
143
|
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"),
|
|
144
|
+
react_1.default.createElement(ink_1.Text, null, " d Delete project (with confirmation)"),
|
|
111
145
|
react_1.default.createElement(ink_1.Text, null, " "),
|
|
112
146
|
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "General:"),
|
|
113
|
-
react_1.default.createElement(ink_1.Text, null, " q/Esc Quit"),
|
|
147
|
+
react_1.default.createElement(ink_1.Text, null, " q/Esc Quit (or close modal)"),
|
|
114
148
|
react_1.default.createElement(ink_1.Text, null, " ? Show this help"),
|
|
115
149
|
react_1.default.createElement(ink_1.Text, null, " "),
|
|
116
150
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Press any key to close...")));
|
|
@@ -120,6 +154,157 @@ const LoadingModal = ({ message }) => {
|
|
|
120
154
|
react_1.default.createElement(ink_1.Text, null, message),
|
|
121
155
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Please wait...")));
|
|
122
156
|
};
|
|
157
|
+
const ConfirmModal = ({ title, message, onConfirm, onCancel }) => {
|
|
158
|
+
const [selected, setSelected] = (0, react_1.useState)('no');
|
|
159
|
+
(0, ink_1.useInput)((input, key) => {
|
|
160
|
+
if (key.escape || input === 'n') {
|
|
161
|
+
onCancel();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (key.return) {
|
|
165
|
+
if (selected === 'yes') {
|
|
166
|
+
onConfirm();
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
onCancel();
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (input === 'y') {
|
|
174
|
+
onConfirm();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (key.leftArrow || key.rightArrow || input === 'h' || input === 'l') {
|
|
178
|
+
setSelected(prev => prev === 'yes' ? 'no' : 'yes');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentOrange, padding: 1, width: 60 },
|
|
182
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentOrange }, title),
|
|
183
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
184
|
+
react_1.default.createElement(ink_1.Text, null, message),
|
|
185
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
186
|
+
react_1.default.createElement(ink_1.Box, null,
|
|
187
|
+
react_1.default.createElement(ink_1.Text, { color: selected === 'yes' ? colors.accentCyan : colors.textSecondary },
|
|
188
|
+
selected === 'yes' ? '▶ ' : ' ',
|
|
189
|
+
"Yes"),
|
|
190
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
191
|
+
react_1.default.createElement(ink_1.Text, { color: selected === 'no' ? colors.accentCyan : colors.textSecondary },
|
|
192
|
+
selected === 'no' ? '▶ ' : ' ',
|
|
193
|
+
"No")),
|
|
194
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
195
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "y/n or \u2190\u2192 to select, Enter to confirm")));
|
|
196
|
+
};
|
|
197
|
+
const AddProjectModal = ({ onAdd, onCancel }) => {
|
|
198
|
+
const [step, setStep] = (0, react_1.useState)('path');
|
|
199
|
+
const [pathInput, setPathInput] = (0, react_1.useState)('');
|
|
200
|
+
const [nameInput, setNameInput] = (0, react_1.useState)('');
|
|
201
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
202
|
+
(0, ink_1.useInput)((input, key) => {
|
|
203
|
+
if (key.escape) {
|
|
204
|
+
onCancel();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (step === 'path') {
|
|
208
|
+
if (key.return) {
|
|
209
|
+
// Validate path
|
|
210
|
+
const resolvedPath = pathInput.startsWith('~')
|
|
211
|
+
? path.join(os.homedir(), pathInput.slice(1))
|
|
212
|
+
: path.resolve(pathInput);
|
|
213
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
214
|
+
setError('Path does not exist');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!fs.statSync(resolvedPath).isDirectory()) {
|
|
218
|
+
setError('Path is not a directory');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Check if project already exists
|
|
222
|
+
const db = (0, core_bridge_1.getDatabaseManager)();
|
|
223
|
+
const existing = db.getProjectByPath(resolvedPath);
|
|
224
|
+
if (existing) {
|
|
225
|
+
setError(`Project already exists: ${existing.name}`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
setError(null);
|
|
229
|
+
setNameInput(path.basename(resolvedPath));
|
|
230
|
+
setStep('name');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (key.backspace || key.delete) {
|
|
234
|
+
setPathInput(prev => prev.slice(0, -1));
|
|
235
|
+
setError(null);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (key.tab) {
|
|
239
|
+
// Tab completion - get directories in current path
|
|
240
|
+
try {
|
|
241
|
+
const currentPath = pathInput.startsWith('~')
|
|
242
|
+
? path.join(os.homedir(), pathInput.slice(1))
|
|
243
|
+
: pathInput || '.';
|
|
244
|
+
const dir = path.dirname(currentPath);
|
|
245
|
+
const base = path.basename(currentPath);
|
|
246
|
+
if (fs.existsSync(dir)) {
|
|
247
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
248
|
+
.filter(e => e.isDirectory() && e.name.startsWith(base) && !e.name.startsWith('.'))
|
|
249
|
+
.map(e => e.name);
|
|
250
|
+
if (entries.length === 1) {
|
|
251
|
+
const completed = path.join(dir, entries[0]) + '/';
|
|
252
|
+
setPathInput(completed.replace(os.homedir(), '~'));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Ignore tab completion errors
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
262
|
+
setPathInput(prev => prev + input);
|
|
263
|
+
setError(null);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else if (step === 'name') {
|
|
267
|
+
if (key.return) {
|
|
268
|
+
const resolvedPath = pathInput.startsWith('~')
|
|
269
|
+
? path.join(os.homedir(), pathInput.slice(1))
|
|
270
|
+
: path.resolve(pathInput);
|
|
271
|
+
onAdd(resolvedPath, nameInput.trim() || undefined);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (key.backspace || key.delete) {
|
|
275
|
+
setNameInput(prev => prev.slice(0, -1));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
279
|
+
setNameInput(prev => prev + input);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentCyan, padding: 1, width: 70 },
|
|
284
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, "Add Project"),
|
|
285
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
286
|
+
step === 'path' && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
287
|
+
react_1.default.createElement(ink_1.Text, null, "Enter project path:"),
|
|
288
|
+
react_1.default.createElement(ink_1.Box, null,
|
|
289
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "> "),
|
|
290
|
+
react_1.default.createElement(ink_1.Text, null, pathInput),
|
|
291
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "_")),
|
|
292
|
+
error && (react_1.default.createElement(ink_1.Text, { color: "#f85149" }, error)),
|
|
293
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
294
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Tab: autocomplete | Enter: next | Esc: cancel"))),
|
|
295
|
+
step === 'name' && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
296
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary },
|
|
297
|
+
"Path: ",
|
|
298
|
+
pathInput),
|
|
299
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
300
|
+
react_1.default.createElement(ink_1.Text, null, "Enter project name:"),
|
|
301
|
+
react_1.default.createElement(ink_1.Box, null,
|
|
302
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "> "),
|
|
303
|
+
react_1.default.createElement(ink_1.Text, null, nameInput),
|
|
304
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "_")),
|
|
305
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
306
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Enter: add project | Esc: cancel")))));
|
|
307
|
+
};
|
|
123
308
|
const ErrorModal = ({ message, onClose }) => {
|
|
124
309
|
(0, ink_1.useInput)((input, key) => {
|
|
125
310
|
if (key.escape || key.return) {
|
|
@@ -176,7 +361,7 @@ const ScriptSelectionModal = ({ scripts, projectName, projectPath, onSelect, onC
|
|
|
176
361
|
react_1.default.createElement(ink_1.Text, null, " "),
|
|
177
362
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "\u2191\u2193/kj: Navigate | Enter: Run | b: Background | Esc/q: Cancel")));
|
|
178
363
|
};
|
|
179
|
-
const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFocused, height, scrollOffset, }) => {
|
|
364
|
+
const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFocused, height, scrollOffset, gitBranches, filterType, sortType, }) => {
|
|
180
365
|
const { focus } = (0, ink_1.useFocus)({ id: 'projectList' });
|
|
181
366
|
// Calculate visible range
|
|
182
367
|
const startIndex = Math.max(0, scrollOffset);
|
|
@@ -189,10 +374,15 @@ const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFoc
|
|
|
189
374
|
const visibleProjects = projects.slice(startIndex, endIndex);
|
|
190
375
|
const hasMoreBelow = endIndex < projects.length;
|
|
191
376
|
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.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
377
|
+
react_1.default.createElement(ink_1.Box, { flexDirection: "column" },
|
|
378
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.textPrimary },
|
|
379
|
+
"Projects (",
|
|
380
|
+
projects.length,
|
|
381
|
+
")"),
|
|
382
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
383
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentPurple }, FILTER_LABELS[filterType]),
|
|
384
|
+
' | ',
|
|
385
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentOrange }, SORT_LABELS[sortType]))),
|
|
196
386
|
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
387
|
hasMoreAbove && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
198
388
|
"\u2191 ",
|
|
@@ -204,28 +394,37 @@ const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFoc
|
|
|
204
394
|
// Check if this project has running scripts
|
|
205
395
|
const projectRunning = runningProcesses.filter((p) => p.projectPath === project.path);
|
|
206
396
|
const hasRunningScripts = projectRunning.length > 0;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
397
|
+
// Get git branch
|
|
398
|
+
const branch = gitBranches.get(project.id);
|
|
399
|
+
const isMainBranch = branch === 'main' || branch === 'master';
|
|
400
|
+
return (react_1.default.createElement(ink_1.Box, { key: project.id, flexDirection: "column" },
|
|
401
|
+
react_1.default.createElement(ink_1.Text, { color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
|
|
402
|
+
isSelected ? '▶ ' : ' ',
|
|
403
|
+
hasRunningScripts && react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF "),
|
|
404
|
+
truncateText(project.name, 22),
|
|
405
|
+
hasRunningScripts && react_1.default.createElement(ink_1.Text, { color: colors.accentGreen },
|
|
406
|
+
" (",
|
|
407
|
+
projectRunning.length,
|
|
408
|
+
")")),
|
|
409
|
+
branch && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
410
|
+
' ',
|
|
411
|
+
react_1.default.createElement(ink_1.Text, { color: isMainBranch ? colors.accentGreen : colors.accentBlue }, truncateText(branch, 18))))));
|
|
215
412
|
}),
|
|
216
413
|
hasMoreBelow && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
217
414
|
"\u2193 ",
|
|
218
415
|
projects.length - endIndex,
|
|
219
416
|
" more below")))))));
|
|
220
417
|
};
|
|
221
|
-
const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editingName, editingDescription, editingTags, editInput, allTags, onTagRemove, height, scrollOffset, }) => {
|
|
418
|
+
const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editingName, editingDescription, editingTags, editInput, allTags, onTagRemove, height, scrollOffset, gitBranch, }) => {
|
|
222
419
|
const { focus } = (0, ink_1.useFocus)({ id: 'projectDetails' });
|
|
223
420
|
const [scripts, setScripts] = (0, react_1.useState)(null);
|
|
224
421
|
const [ports, setPorts] = (0, react_1.useState)([]);
|
|
422
|
+
const [npmPackage, setNpmPackage] = (0, react_1.useState)(null);
|
|
225
423
|
(0, react_1.useEffect)(() => {
|
|
226
424
|
if (!project) {
|
|
227
425
|
setScripts(null);
|
|
228
426
|
setPorts([]);
|
|
427
|
+
setNpmPackage(null);
|
|
229
428
|
return;
|
|
230
429
|
}
|
|
231
430
|
// Load scripts
|
|
@@ -245,6 +444,26 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
|
|
|
245
444
|
catch (error) {
|
|
246
445
|
setPorts([]);
|
|
247
446
|
}
|
|
447
|
+
// Check if npm package (live registry check)
|
|
448
|
+
setNpmPackage(null);
|
|
449
|
+
try {
|
|
450
|
+
const packageJsonPath = path.join(project.path, 'package.json');
|
|
451
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
452
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
453
|
+
if (pkg.name && !pkg.private) {
|
|
454
|
+
fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`, {
|
|
455
|
+
method: 'HEAD',
|
|
456
|
+
signal: AbortSignal.timeout(2000),
|
|
457
|
+
})
|
|
458
|
+
.then(res => {
|
|
459
|
+
if (res.ok)
|
|
460
|
+
setNpmPackage(pkg.name);
|
|
461
|
+
})
|
|
462
|
+
.catch(() => { });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch { }
|
|
248
467
|
}, [project]);
|
|
249
468
|
if (!project) {
|
|
250
469
|
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: isFocused ? colors.accentCyan : colors.borderColor, padding: 1 },
|
|
@@ -299,8 +518,20 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
|
|
|
299
518
|
react_1.default.createElement(ink_1.Text, null, " | "),
|
|
300
519
|
react_1.default.createElement(ink_1.Text, null,
|
|
301
520
|
"Scripts: ",
|
|
302
|
-
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts?.scripts?.size || 0))
|
|
521
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts?.scripts?.size || 0)),
|
|
522
|
+
npmPackage && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
523
|
+
react_1.default.createElement(ink_1.Text, null, " | "),
|
|
524
|
+
react_1.default.createElement(ink_1.Text, null,
|
|
525
|
+
"NPM: ",
|
|
526
|
+
react_1.default.createElement(ink_1.Text, { color: "#f85149" }, npmPackage))))));
|
|
303
527
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer2" }, " "));
|
|
528
|
+
// Git branch
|
|
529
|
+
if (gitBranch) {
|
|
530
|
+
const isMainBranch = gitBranch === 'main' || gitBranch === 'master';
|
|
531
|
+
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "git-branch" },
|
|
532
|
+
"Branch: ",
|
|
533
|
+
react_1.default.createElement(ink_1.Text, { color: isMainBranch ? colors.accentGreen : colors.accentBlue }, gitBranch)));
|
|
534
|
+
}
|
|
304
535
|
if (project.framework) {
|
|
305
536
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "framework" },
|
|
306
537
|
"Framework: ",
|
|
@@ -335,34 +566,28 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
|
|
|
335
566
|
});
|
|
336
567
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer4" }, " "));
|
|
337
568
|
}
|
|
338
|
-
// Scripts
|
|
569
|
+
// Scripts - show all, let virtual scrolling handle visibility
|
|
339
570
|
if (scripts && scripts.scripts && scripts.scripts.size > 0) {
|
|
340
571
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "scripts-header", bold: true },
|
|
341
572
|
"Available Scripts (",
|
|
342
573
|
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts.scripts.size),
|
|
343
574
|
"):"));
|
|
344
|
-
Array.from(scripts.scripts.entries()).
|
|
575
|
+
Array.from(scripts.scripts.entries()).forEach(([name, script]) => {
|
|
345
576
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: `script-${name}` },
|
|
346
577
|
' ',
|
|
347
578
|
react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, name),
|
|
348
579
|
' - ',
|
|
349
580
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, truncateText(script.command, 60))));
|
|
350
581
|
});
|
|
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
582
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer5" }, " "));
|
|
358
583
|
}
|
|
359
|
-
// Ports
|
|
584
|
+
// Ports - show all, let virtual scrolling handle visibility
|
|
360
585
|
if (ports.length > 0) {
|
|
361
586
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "ports-header", bold: true },
|
|
362
587
|
"Detected Ports (",
|
|
363
588
|
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, ports.length),
|
|
364
589
|
"):"));
|
|
365
|
-
ports.
|
|
590
|
+
ports.forEach((port) => {
|
|
366
591
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: `port-${port.id}` },
|
|
367
592
|
' ',
|
|
368
593
|
"Port ",
|
|
@@ -371,12 +596,6 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
|
|
|
371
596
|
" - ",
|
|
372
597
|
truncateText(port.config_source, 50))));
|
|
373
598
|
});
|
|
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
599
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer6" }, " "));
|
|
381
600
|
}
|
|
382
601
|
// Calculate visible range for virtual scrolling
|
|
@@ -404,6 +623,80 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
|
|
|
404
623
|
contentLines.length - endIndex,
|
|
405
624
|
" more below")))));
|
|
406
625
|
};
|
|
626
|
+
const TerminalOutputPanel = ({ processes, selectedPid, height, onSelectProcess, }) => {
|
|
627
|
+
const [logs, setLogs] = (0, react_1.useState)([]);
|
|
628
|
+
const [scrollOffset, setScrollOffset] = (0, react_1.useState)(0);
|
|
629
|
+
// Find the selected process or default to first
|
|
630
|
+
const activeProcess = selectedPid
|
|
631
|
+
? processes.find((p) => p.pid === selectedPid)
|
|
632
|
+
: processes[0];
|
|
633
|
+
(0, react_1.useEffect)(() => {
|
|
634
|
+
if (!activeProcess?.logFile) {
|
|
635
|
+
setLogs(['No active process selected']);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
// Read initial logs
|
|
639
|
+
try {
|
|
640
|
+
if (fs.existsSync(activeProcess.logFile)) {
|
|
641
|
+
const content = fs.readFileSync(activeProcess.logFile, 'utf-8');
|
|
642
|
+
const lines = content.split('\n').slice(-50); // Last 50 lines
|
|
643
|
+
setLogs(lines);
|
|
644
|
+
setScrollOffset(Math.max(0, lines.length - 10));
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
setLogs(['Log file not found']);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
setLogs(['Error reading logs']);
|
|
652
|
+
}
|
|
653
|
+
// Watch for changes
|
|
654
|
+
let watcher = null;
|
|
655
|
+
try {
|
|
656
|
+
watcher = fs.watch(activeProcess.logFile, () => {
|
|
657
|
+
try {
|
|
658
|
+
const content = fs.readFileSync(activeProcess.logFile, 'utf-8');
|
|
659
|
+
const lines = content.split('\n').slice(-100);
|
|
660
|
+
setLogs(lines);
|
|
661
|
+
// Auto-scroll to bottom
|
|
662
|
+
setScrollOffset(Math.max(0, lines.length - 10));
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
// Ignore read errors during watch
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
// Ignore watch errors
|
|
671
|
+
}
|
|
672
|
+
return () => {
|
|
673
|
+
if (watcher) {
|
|
674
|
+
watcher.close();
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
}, [activeProcess?.logFile, activeProcess?.pid]);
|
|
678
|
+
const visibleLines = logs.slice(scrollOffset, scrollOffset + height - 4);
|
|
679
|
+
const hasMoreAbove = scrollOffset > 0;
|
|
680
|
+
const hasMoreBelow = scrollOffset + height - 4 < logs.length;
|
|
681
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "30%", height: height, borderStyle: "round", borderColor: colors.accentGreen, padding: 1, flexShrink: 0 },
|
|
682
|
+
react_1.default.createElement(ink_1.Box, { flexDirection: "row", justifyContent: "space-between" },
|
|
683
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentGreen }, "Terminal Output"),
|
|
684
|
+
processes.length > 1 && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
685
|
+
"[",
|
|
686
|
+
processes.findIndex((p) => p.pid === activeProcess?.pid) + 1,
|
|
687
|
+
"/",
|
|
688
|
+
processes.length,
|
|
689
|
+
"]"))),
|
|
690
|
+
activeProcess && (react_1.default.createElement(ink_1.Text, { color: colors.textSecondary },
|
|
691
|
+
truncateText(activeProcess.scriptName, 20),
|
|
692
|
+
" (PID: ",
|
|
693
|
+
activeProcess.pid,
|
|
694
|
+
")")),
|
|
695
|
+
react_1.default.createElement(ink_1.Box, { flexDirection: "column", flexGrow: 1, marginTop: 1 },
|
|
696
|
+
hasMoreAbove && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "\u2191 more above")),
|
|
697
|
+
visibleLines.map((line, idx) => (react_1.default.createElement(ink_1.Text, { key: idx, color: colors.textPrimary }, truncateText(line, 40)))),
|
|
698
|
+
hasMoreBelow && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "\u2193 more below")))));
|
|
699
|
+
};
|
|
407
700
|
const StatusBar = ({ focusedPanel, selectedProject }) => {
|
|
408
701
|
if (focusedPanel === 'list') {
|
|
409
702
|
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column" },
|
|
@@ -411,16 +704,22 @@ const StatusBar = ({ focusedPanel, selectedProject }) => {
|
|
|
411
704
|
react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF API"),
|
|
412
705
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " | "),
|
|
413
706
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Focus: "),
|
|
414
|
-
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "
|
|
707
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "List")),
|
|
415
708
|
react_1.default.createElement(ink_1.Box, null,
|
|
709
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "a"),
|
|
710
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Add | "),
|
|
416
711
|
react_1.default.createElement(ink_1.Text, { bold: true }, "/"),
|
|
417
712
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Search | "),
|
|
418
|
-
react_1.default.createElement(ink_1.Text, { bold: true }, "
|
|
419
|
-
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "
|
|
420
|
-
react_1.default.createElement(ink_1.Text, { bold: true }, "
|
|
713
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "F"),
|
|
714
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Filter | "),
|
|
715
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "S"),
|
|
716
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Sort | "),
|
|
717
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "\u2191\u2193"),
|
|
718
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Nav | "),
|
|
719
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "Tab"),
|
|
421
720
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Switch | "),
|
|
422
|
-
react_1.default.createElement(ink_1.Text, { bold: true }, "
|
|
423
|
-
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "
|
|
721
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "T"),
|
|
722
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Terminal | "),
|
|
424
723
|
react_1.default.createElement(ink_1.Text, { bold: true }, "?"),
|
|
425
724
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Help | "),
|
|
426
725
|
react_1.default.createElement(ink_1.Text, { bold: true }, "q"),
|
|
@@ -435,26 +734,18 @@ const StatusBar = ({ focusedPanel, selectedProject }) => {
|
|
|
435
734
|
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Details"),
|
|
436
735
|
selectedProject && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
437
736
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " | "),
|
|
438
|
-
react_1.default.createElement(ink_1.Text, { color: colors.textPrimary }, selectedProject.name)))),
|
|
737
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textPrimary }, truncateText(selectedProject.name, 20))))),
|
|
439
738
|
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
739
|
react_1.default.createElement(ink_1.Text, { bold: true }, "e"),
|
|
443
740
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Edit | "),
|
|
444
741
|
react_1.default.createElement(ink_1.Text, { bold: true }, "t"),
|
|
445
742
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Tags | "),
|
|
446
743
|
react_1.default.createElement(ink_1.Text, { bold: true }, "o"),
|
|
447
744
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Editor | "),
|
|
448
|
-
react_1.default.createElement(ink_1.Text, { bold: true }, "
|
|
449
|
-
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "
|
|
450
|
-
react_1.default.createElement(ink_1.Text, { bold: true }, "u"),
|
|
451
|
-
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " URLs | "),
|
|
745
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "r"),
|
|
746
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Run | "),
|
|
452
747
|
react_1.default.createElement(ink_1.Text, { bold: true }, "s"),
|
|
453
748
|
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
749
|
react_1.default.createElement(ink_1.Text, { bold: true }, "x"),
|
|
459
750
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Stop | "),
|
|
460
751
|
react_1.default.createElement(ink_1.Text, { bold: true }, "d"),
|
|
@@ -462,9 +753,7 @@ const StatusBar = ({ focusedPanel, selectedProject }) => {
|
|
|
462
753
|
react_1.default.createElement(ink_1.Text, { bold: true }, "Tab"),
|
|
463
754
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Switch | "),
|
|
464
755
|
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"))));
|
|
756
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Help"))));
|
|
468
757
|
};
|
|
469
758
|
// Simple fuzzy search function
|
|
470
759
|
function fuzzyMatch(query, text) {
|
|
@@ -492,6 +781,13 @@ const App = () => {
|
|
|
492
781
|
const [error, setError] = (0, react_1.useState)(null);
|
|
493
782
|
const [runningProcesses, setRunningProcesses] = (0, react_1.useState)([]);
|
|
494
783
|
const [focusedPanel, setFocusedPanel] = (0, react_1.useState)('list');
|
|
784
|
+
// View state
|
|
785
|
+
const [currentView, setCurrentView] = (0, react_1.useState)('projects');
|
|
786
|
+
// Git branches
|
|
787
|
+
const [gitBranches, setGitBranches] = (0, react_1.useState)(new Map());
|
|
788
|
+
// Filter and sort state
|
|
789
|
+
const [filterType, setFilterType] = (0, react_1.useState)('all');
|
|
790
|
+
const [sortType, setSortType] = (0, react_1.useState)('name-asc');
|
|
495
791
|
// Editing state
|
|
496
792
|
const [editingName, setEditingName] = (0, react_1.useState)(false);
|
|
497
793
|
const [editingDescription, setEditingDescription] = (0, react_1.useState)(false);
|
|
@@ -499,6 +795,9 @@ const App = () => {
|
|
|
499
795
|
const [editInput, setEditInput] = (0, react_1.useState)('');
|
|
500
796
|
const [showUrls, setShowUrls] = (0, react_1.useState)(false);
|
|
501
797
|
const [allTags, setAllTags] = (0, react_1.useState)([]);
|
|
798
|
+
// Modal state
|
|
799
|
+
const [showAddProjectModal, setShowAddProjectModal] = (0, react_1.useState)(false);
|
|
800
|
+
const [showConfirmDelete, setShowConfirmDelete] = (0, react_1.useState)(false);
|
|
502
801
|
// Search state
|
|
503
802
|
const [showSearch, setShowSearch] = (0, react_1.useState)(false);
|
|
504
803
|
const [searchQuery, setSearchQuery] = (0, react_1.useState)('');
|
|
@@ -507,19 +806,69 @@ const App = () => {
|
|
|
507
806
|
// Script selection state
|
|
508
807
|
const [showScriptModal, setShowScriptModal] = (0, react_1.useState)(false);
|
|
509
808
|
const [scriptModalData, setScriptModalData] = (0, react_1.useState)(null);
|
|
809
|
+
// Workspace state
|
|
810
|
+
const [workspaces, setWorkspaces] = (0, react_1.useState)([]);
|
|
811
|
+
const [selectedWorkspaceIndex, setSelectedWorkspaceIndex] = (0, react_1.useState)(0);
|
|
812
|
+
// Terminal panel state
|
|
813
|
+
const [showTerminalPanel, setShowTerminalPanel] = (0, react_1.useState)(false);
|
|
814
|
+
const [terminalLogs, setTerminalLogs] = (0, react_1.useState)([]);
|
|
815
|
+
const [selectedProcessPid, setSelectedProcessPid] = (0, react_1.useState)(null);
|
|
816
|
+
// Settings state
|
|
817
|
+
const [settings, setSettings] = (0, react_1.useState)({
|
|
818
|
+
editor: { type: 'vscode' },
|
|
819
|
+
browser: { type: 'chrome' },
|
|
820
|
+
});
|
|
821
|
+
const [settingsSection, setSettingsSection] = (0, react_1.useState)('editor');
|
|
822
|
+
const [settingsOptionIndex, setSettingsOptionIndex] = (0, react_1.useState)(0);
|
|
823
|
+
const settingsEditorOptions = [
|
|
824
|
+
'vscode', 'cursor', 'windsurf', 'zed', 'custom',
|
|
825
|
+
];
|
|
826
|
+
const settingsBrowserOptions = [
|
|
827
|
+
'chrome', 'firefox', 'safari', 'edge', 'custom',
|
|
828
|
+
];
|
|
510
829
|
// Get terminal dimensions
|
|
511
830
|
const terminalHeight = process.stdout.rows || 24;
|
|
512
|
-
const availableHeight = terminalHeight -
|
|
831
|
+
const availableHeight = terminalHeight - 4; // Subtract status bar (increased for view indicator)
|
|
513
832
|
(0, react_1.useEffect)(() => {
|
|
514
833
|
loadProjects();
|
|
515
834
|
loadRunningProcesses();
|
|
516
835
|
loadAllTags();
|
|
517
|
-
//
|
|
836
|
+
// Load settings
|
|
837
|
+
try {
|
|
838
|
+
const settingsPath = path.join(os.homedir(), '.projax', 'settings.json');
|
|
839
|
+
if (fs.existsSync(settingsPath)) {
|
|
840
|
+
const data = fs.readFileSync(settingsPath, 'utf-8');
|
|
841
|
+
setSettings(JSON.parse(data));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
// Use defaults
|
|
846
|
+
}
|
|
847
|
+
// Refresh running processes and git branches every 5 seconds
|
|
518
848
|
const interval = setInterval(() => {
|
|
519
849
|
loadRunningProcesses();
|
|
520
850
|
}, 5000);
|
|
521
851
|
return () => clearInterval(interval);
|
|
522
852
|
}, []);
|
|
853
|
+
// Load git branches when projects change
|
|
854
|
+
(0, react_1.useEffect)(() => {
|
|
855
|
+
if (allProjects.length > 0) {
|
|
856
|
+
loadGitBranches();
|
|
857
|
+
}
|
|
858
|
+
}, [allProjects]);
|
|
859
|
+
const loadGitBranches = async () => {
|
|
860
|
+
const branches = new Map();
|
|
861
|
+
for (const project of allProjects) {
|
|
862
|
+
try {
|
|
863
|
+
const branch = (0, core_bridge_1.getCurrentBranch)(project.path);
|
|
864
|
+
branches.set(project.id, branch);
|
|
865
|
+
}
|
|
866
|
+
catch {
|
|
867
|
+
branches.set(project.id, null);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
setGitBranches(branches);
|
|
871
|
+
};
|
|
523
872
|
// Reset editing state and scroll when project changes
|
|
524
873
|
(0, react_1.useEffect)(() => {
|
|
525
874
|
setEditingName(false);
|
|
@@ -528,6 +877,12 @@ const App = () => {
|
|
|
528
877
|
setEditInput('');
|
|
529
878
|
setDetailsScrollOffset(0); // Reset scroll when switching projects
|
|
530
879
|
}, [selectedIndex]);
|
|
880
|
+
// Load workspaces when switching to workspaces view
|
|
881
|
+
(0, react_1.useEffect)(() => {
|
|
882
|
+
if (currentView === 'workspaces' && workspaces.length === 0) {
|
|
883
|
+
loadWorkspacesFromApi();
|
|
884
|
+
}
|
|
885
|
+
}, [currentView]);
|
|
531
886
|
// Update scroll offset when selected index changes
|
|
532
887
|
(0, react_1.useEffect)(() => {
|
|
533
888
|
const visibleHeight = Math.max(1, availableHeight - 3);
|
|
@@ -560,26 +915,71 @@ const App = () => {
|
|
|
560
915
|
const loadProjects = () => {
|
|
561
916
|
const loadedProjects = (0, core_bridge_1.getAllProjects)();
|
|
562
917
|
setAllProjects(loadedProjects);
|
|
563
|
-
|
|
918
|
+
applyFilterAndSort(loadedProjects, searchQuery, filterType, sortType);
|
|
564
919
|
};
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
920
|
+
const applyFilterAndSort = (projectsToFilter, query, filter, sort) => {
|
|
921
|
+
let filtered = projectsToFilter;
|
|
922
|
+
// Apply search query with filter type
|
|
923
|
+
if (query.trim()) {
|
|
924
|
+
const q = query.toLowerCase();
|
|
925
|
+
filtered = projectsToFilter.filter(project => {
|
|
926
|
+
switch (filter) {
|
|
927
|
+
case 'name':
|
|
928
|
+
return fuzzyMatch(q, project.name);
|
|
929
|
+
case 'path':
|
|
930
|
+
return fuzzyMatch(q, project.path);
|
|
931
|
+
case 'tags':
|
|
932
|
+
return project.tags?.some((tag) => fuzzyMatch(q, tag)) || false;
|
|
933
|
+
case 'ports': {
|
|
934
|
+
// Check if project has ports matching the query
|
|
935
|
+
const db = (0, core_bridge_1.getDatabaseManager)();
|
|
936
|
+
const ports = db.getProjectPorts(project.id);
|
|
937
|
+
return ports.some((p) => p.port.toString().includes(q));
|
|
938
|
+
}
|
|
939
|
+
case 'running': {
|
|
940
|
+
const isRunning = runningProcesses.some((p) => p.projectPath === project.path);
|
|
941
|
+
return (q === 'running' || q === 'yes' || q === 'true') ? isRunning : !isRunning;
|
|
942
|
+
}
|
|
943
|
+
case 'all':
|
|
944
|
+
default:
|
|
945
|
+
return (fuzzyMatch(q, project.name) ||
|
|
946
|
+
(project.description ? fuzzyMatch(q, project.description) : false) ||
|
|
947
|
+
fuzzyMatch(q, project.path) ||
|
|
948
|
+
project.tags?.some((tag) => fuzzyMatch(q, tag)) ||
|
|
949
|
+
false);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
569
952
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
953
|
+
// Apply sorting
|
|
954
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
955
|
+
switch (sort) {
|
|
956
|
+
case 'name-asc':
|
|
957
|
+
return a.name.localeCompare(b.name);
|
|
958
|
+
case 'name-desc':
|
|
959
|
+
return b.name.localeCompare(a.name);
|
|
960
|
+
case 'recent':
|
|
961
|
+
return (b.last_scanned || 0) - (a.last_scanned || 0);
|
|
962
|
+
case 'oldest':
|
|
963
|
+
return (a.created_at || 0) - (b.created_at || 0);
|
|
964
|
+
case 'running': {
|
|
965
|
+
const aRunning = runningProcesses.filter((p) => p.projectPath === a.path).length;
|
|
966
|
+
const bRunning = runningProcesses.filter((p) => p.projectPath === b.path).length;
|
|
967
|
+
return bRunning - aRunning;
|
|
968
|
+
}
|
|
969
|
+
default:
|
|
970
|
+
return 0;
|
|
971
|
+
}
|
|
576
972
|
});
|
|
577
|
-
setProjects(
|
|
973
|
+
setProjects(sorted);
|
|
578
974
|
// Adjust selected index if current selection is out of bounds
|
|
579
|
-
if (selectedIndex >=
|
|
580
|
-
setSelectedIndex(Math.max(0,
|
|
975
|
+
if (selectedIndex >= sorted.length) {
|
|
976
|
+
setSelectedIndex(Math.max(0, sorted.length - 1));
|
|
581
977
|
}
|
|
582
978
|
};
|
|
979
|
+
// Re-apply filter/sort when dependencies change
|
|
980
|
+
(0, react_1.useEffect)(() => {
|
|
981
|
+
applyFilterAndSort(allProjects, searchQuery, filterType, sortType);
|
|
982
|
+
}, [filterType, sortType, runningProcesses]);
|
|
583
983
|
const loadRunningProcesses = async () => {
|
|
584
984
|
try {
|
|
585
985
|
const processes = await (0, script_runner_1.getRunningProcessesClean)();
|
|
@@ -726,13 +1126,144 @@ const App = () => {
|
|
|
726
1126
|
setError(err instanceof Error ? err.message : String(err));
|
|
727
1127
|
}
|
|
728
1128
|
};
|
|
1129
|
+
// Handler for adding a project
|
|
1130
|
+
const handleAddProject = async (projectPath, projectName) => {
|
|
1131
|
+
setShowAddProjectModal(false);
|
|
1132
|
+
setIsLoading(true);
|
|
1133
|
+
setLoadingMessage('Adding project...');
|
|
1134
|
+
try {
|
|
1135
|
+
const name = projectName || path.basename(projectPath);
|
|
1136
|
+
const db = (0, core_bridge_1.getDatabaseManager)();
|
|
1137
|
+
const project = db.addProject(name, projectPath);
|
|
1138
|
+
// Scan for tests
|
|
1139
|
+
setLoadingMessage('Scanning for tests...');
|
|
1140
|
+
await (0, core_bridge_1.scanProject)(project.id);
|
|
1141
|
+
// Scan for ports
|
|
1142
|
+
setLoadingMessage('Scanning for ports...');
|
|
1143
|
+
try {
|
|
1144
|
+
const { scanProjectPorts } = await Promise.resolve().then(() => __importStar(require('./port-scanner')));
|
|
1145
|
+
await scanProjectPorts(project.id);
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
// Ignore port scanning errors
|
|
1149
|
+
}
|
|
1150
|
+
loadProjects();
|
|
1151
|
+
setIsLoading(false);
|
|
1152
|
+
// Select the newly added project
|
|
1153
|
+
const newProjects = (0, core_bridge_1.getAllProjects)();
|
|
1154
|
+
const newIndex = newProjects.findIndex((p) => p.id === project.id);
|
|
1155
|
+
if (newIndex >= 0) {
|
|
1156
|
+
setSelectedIndex(newIndex);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
catch (err) {
|
|
1160
|
+
setIsLoading(false);
|
|
1161
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
// Handler for deleting a project
|
|
1165
|
+
const handleDeleteProject = () => {
|
|
1166
|
+
if (!selectedProject)
|
|
1167
|
+
return;
|
|
1168
|
+
setShowConfirmDelete(false);
|
|
1169
|
+
setIsLoading(true);
|
|
1170
|
+
setLoadingMessage(`Deleting ${selectedProject.name}...`);
|
|
1171
|
+
setTimeout(async () => {
|
|
1172
|
+
try {
|
|
1173
|
+
const db = (0, core_bridge_1.getDatabaseManager)();
|
|
1174
|
+
db.removeProject(selectedProject.id);
|
|
1175
|
+
loadProjects();
|
|
1176
|
+
if (selectedIndex >= projects.length - 1) {
|
|
1177
|
+
setSelectedIndex(Math.max(0, projects.length - 2));
|
|
1178
|
+
}
|
|
1179
|
+
setIsLoading(false);
|
|
1180
|
+
}
|
|
1181
|
+
catch (err) {
|
|
1182
|
+
setIsLoading(false);
|
|
1183
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1184
|
+
}
|
|
1185
|
+
}, 100);
|
|
1186
|
+
};
|
|
1187
|
+
// Cycle filter type
|
|
1188
|
+
const cycleFilterType = () => {
|
|
1189
|
+
const currentIndex = FILTER_TYPES.indexOf(filterType);
|
|
1190
|
+
const nextIndex = (currentIndex + 1) % FILTER_TYPES.length;
|
|
1191
|
+
setFilterType(FILTER_TYPES[nextIndex]);
|
|
1192
|
+
};
|
|
1193
|
+
// Cycle sort type
|
|
1194
|
+
const cycleSortType = () => {
|
|
1195
|
+
const currentIndex = SORT_TYPES.indexOf(sortType);
|
|
1196
|
+
const nextIndex = (currentIndex + 1) % SORT_TYPES.length;
|
|
1197
|
+
setSortType(SORT_TYPES[nextIndex]);
|
|
1198
|
+
};
|
|
729
1199
|
(0, ink_1.useInput)((input, key) => {
|
|
1200
|
+
if (key.mouse) {
|
|
1201
|
+
const { x, y, wheelDown, wheelUp, left } = key.mouse;
|
|
1202
|
+
const { columns: width } = process.stdout;
|
|
1203
|
+
// Assuming project list is on the left 35% and details on the right.
|
|
1204
|
+
const projectListWidth = Math.floor(width * 0.35);
|
|
1205
|
+
if (x < projectListWidth) {
|
|
1206
|
+
// Mouse is over the project list
|
|
1207
|
+
if (wheelUp) {
|
|
1208
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (wheelDown) {
|
|
1212
|
+
setSelectedIndex((prev) => Math.min(projects.length - 1, prev + 1));
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (left) {
|
|
1216
|
+
// It's a click, so we need to calculate which item was clicked.
|
|
1217
|
+
// This is an approximation based on the known layout of the ProjectListComponent.
|
|
1218
|
+
const listTopBorder = 1;
|
|
1219
|
+
const listPadding = 1;
|
|
1220
|
+
const listHeaderHeight = 2; // "Projects (...)" + "Filter | Sort"
|
|
1221
|
+
const listStartY = listTopBorder + listPadding + listHeaderHeight;
|
|
1222
|
+
const scrollIndicatorHeight = listScrollOffset > 0 ? 1 : 0;
|
|
1223
|
+
const firstItemY = listStartY + scrollIndicatorHeight;
|
|
1224
|
+
const clickYInList = y - firstItemY;
|
|
1225
|
+
if (clickYInList >= 0) {
|
|
1226
|
+
let cumulativeHeight = 0;
|
|
1227
|
+
const visibleProjects = projects.slice(listScrollOffset);
|
|
1228
|
+
for (let i = 0; i < visibleProjects.length; i++) {
|
|
1229
|
+
const project = visibleProjects[i];
|
|
1230
|
+
const branch = gitBranches.get(project.id);
|
|
1231
|
+
const itemHeight = branch ? 2 : 1;
|
|
1232
|
+
if (clickYInList >= cumulativeHeight && clickYInList < cumulativeHeight + itemHeight) {
|
|
1233
|
+
const newIndex = listScrollOffset + i;
|
|
1234
|
+
if (newIndex < projects.length) {
|
|
1235
|
+
setSelectedIndex(newIndex);
|
|
1236
|
+
setFocusedPanel('list');
|
|
1237
|
+
}
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
cumulativeHeight += itemHeight;
|
|
1241
|
+
if (cumulativeHeight > availableHeight) {
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
else {
|
|
1250
|
+
// Mouse is over the details panel
|
|
1251
|
+
if (wheelUp) {
|
|
1252
|
+
setDetailsScrollOffset(prev => Math.max(0, prev - 1));
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
if (wheelDown) {
|
|
1256
|
+
setDetailsScrollOffset(prev => prev + 1);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
730
1261
|
// Handle search mode
|
|
731
1262
|
if (showSearch) {
|
|
732
1263
|
if (key.escape) {
|
|
733
1264
|
setShowSearch(false);
|
|
734
1265
|
setSearchQuery('');
|
|
735
|
-
|
|
1266
|
+
applyFilterAndSort(allProjects, '', filterType, sortType);
|
|
736
1267
|
return;
|
|
737
1268
|
}
|
|
738
1269
|
if (key.return) {
|
|
@@ -742,19 +1273,19 @@ const App = () => {
|
|
|
742
1273
|
if (key.backspace || key.delete) {
|
|
743
1274
|
const newQuery = searchQuery.slice(0, -1);
|
|
744
1275
|
setSearchQuery(newQuery);
|
|
745
|
-
|
|
1276
|
+
applyFilterAndSort(allProjects, newQuery, filterType, sortType);
|
|
746
1277
|
return;
|
|
747
1278
|
}
|
|
748
1279
|
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
749
1280
|
const newQuery = searchQuery + input;
|
|
750
1281
|
setSearchQuery(newQuery);
|
|
751
|
-
|
|
1282
|
+
applyFilterAndSort(allProjects, newQuery, filterType, sortType);
|
|
752
1283
|
return;
|
|
753
1284
|
}
|
|
754
1285
|
return;
|
|
755
1286
|
}
|
|
756
1287
|
// Don't process input if modal is showing
|
|
757
|
-
if (showHelp || isLoading || error || showUrls || showScriptModal) {
|
|
1288
|
+
if (showHelp || isLoading || error || showUrls || showScriptModal || showAddProjectModal || showConfirmDelete) {
|
|
758
1289
|
// Handle URLs modal
|
|
759
1290
|
if (showUrls && (key.escape || key.return || input === 'q' || input === 'u')) {
|
|
760
1291
|
setShowUrls(false);
|
|
@@ -762,12 +1293,135 @@ const App = () => {
|
|
|
762
1293
|
}
|
|
763
1294
|
return;
|
|
764
1295
|
}
|
|
1296
|
+
// Handle navigation in workspaces view
|
|
1297
|
+
if (currentView === 'workspaces') {
|
|
1298
|
+
if (key.upArrow || input === 'k') {
|
|
1299
|
+
setSelectedWorkspaceIndex((prev) => Math.max(0, prev - 1));
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (key.downArrow || input === 'j') {
|
|
1303
|
+
setSelectedWorkspaceIndex((prev) => Math.min(workspaces.length - 1, prev + 1));
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
// Handle navigation in processes view
|
|
1308
|
+
if (currentView === 'processes') {
|
|
1309
|
+
if (input === 'x' && runningProcesses.length > 0) {
|
|
1310
|
+
// Stop all processes (or could select one)
|
|
1311
|
+
setIsLoading(true);
|
|
1312
|
+
setLoadingMessage('Stopping processes...');
|
|
1313
|
+
setTimeout(async () => {
|
|
1314
|
+
try {
|
|
1315
|
+
for (const proc of runningProcesses) {
|
|
1316
|
+
await (0, script_runner_1.stopScript)(proc.pid);
|
|
1317
|
+
}
|
|
1318
|
+
await loadRunningProcesses();
|
|
1319
|
+
setIsLoading(false);
|
|
1320
|
+
}
|
|
1321
|
+
catch (err) {
|
|
1322
|
+
setIsLoading(false);
|
|
1323
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1324
|
+
}
|
|
1325
|
+
}, 100);
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
// Handle navigation in settings view
|
|
1330
|
+
if (currentView === 'settings') {
|
|
1331
|
+
if (key.tab) {
|
|
1332
|
+
setSettingsSection((prev) => (prev === 'editor' ? 'browser' : 'editor'));
|
|
1333
|
+
setSettingsOptionIndex(0);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (key.upArrow || input === 'k') {
|
|
1337
|
+
setSettingsOptionIndex((prev) => Math.max(0, prev - 1));
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (key.downArrow || input === 'j') {
|
|
1341
|
+
const maxIndex = settingsSection === 'editor' ? settingsEditorOptions.length - 1 : settingsBrowserOptions.length - 1;
|
|
1342
|
+
setSettingsOptionIndex((prev) => Math.min(maxIndex, prev + 1));
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (input === ' ' || key.return) {
|
|
1346
|
+
// Select option and save
|
|
1347
|
+
const newSettings = { ...settings };
|
|
1348
|
+
if (settingsSection === 'editor') {
|
|
1349
|
+
newSettings.editor = { type: settingsEditorOptions[settingsOptionIndex] };
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
newSettings.browser = { type: settingsBrowserOptions[settingsOptionIndex] };
|
|
1353
|
+
}
|
|
1354
|
+
setSettings(newSettings);
|
|
1355
|
+
// Save to file
|
|
1356
|
+
try {
|
|
1357
|
+
const settingsDir = path.join(os.homedir(), '.projax');
|
|
1358
|
+
if (!fs.existsSync(settingsDir)) {
|
|
1359
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
1360
|
+
}
|
|
1361
|
+
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
1362
|
+
fs.writeFileSync(settingsPath, JSON.stringify(newSettings, null, 2));
|
|
1363
|
+
}
|
|
1364
|
+
catch {
|
|
1365
|
+
// Ignore save errors
|
|
1366
|
+
}
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
// Global navigation - number keys for view switching
|
|
1371
|
+
if (input === '1') {
|
|
1372
|
+
setCurrentView('projects');
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
if (input === '2') {
|
|
1376
|
+
setCurrentView('workspaces');
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (input === '3') {
|
|
1380
|
+
setCurrentView('processes');
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
if (input === '4') {
|
|
1384
|
+
setCurrentView('settings');
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
// Terminal panel toggle
|
|
1388
|
+
if (input === 'T') {
|
|
1389
|
+
setShowTerminalPanel(prev => !prev);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
765
1392
|
// Search shortcut
|
|
766
1393
|
if (input === '/') {
|
|
767
1394
|
setShowSearch(true);
|
|
768
1395
|
setSearchQuery('');
|
|
769
1396
|
return;
|
|
770
1397
|
}
|
|
1398
|
+
// Add project shortcut (in projects view)
|
|
1399
|
+
if (input === 'a' && currentView === 'projects') {
|
|
1400
|
+
setShowAddProjectModal(true);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
// Filter cycle shortcut
|
|
1404
|
+
if (input === 'F' && currentView === 'projects') {
|
|
1405
|
+
cycleFilterType();
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
// Sort cycle shortcut
|
|
1409
|
+
if (input === 'S' && currentView === 'projects') {
|
|
1410
|
+
cycleSortType();
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
// Refresh git branches
|
|
1414
|
+
if (input === 'g' && currentView === 'projects') {
|
|
1415
|
+
loadGitBranches();
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
// Full refresh
|
|
1419
|
+
if (input === 'R') {
|
|
1420
|
+
loadProjects();
|
|
1421
|
+
loadRunningProcesses();
|
|
1422
|
+
loadGitBranches();
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
771
1425
|
// Handle editing modes
|
|
772
1426
|
if (editingName || editingDescription || editingTags) {
|
|
773
1427
|
if (key.escape) {
|
|
@@ -924,25 +1578,9 @@ const App = () => {
|
|
|
924
1578
|
setShowUrls(true);
|
|
925
1579
|
return;
|
|
926
1580
|
}
|
|
927
|
-
// Delete project
|
|
1581
|
+
// Delete project (with confirmation)
|
|
928
1582
|
if (input === 'd') {
|
|
929
|
-
|
|
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);
|
|
1583
|
+
setShowConfirmDelete(true);
|
|
946
1584
|
return;
|
|
947
1585
|
}
|
|
948
1586
|
}
|
|
@@ -1076,6 +1714,14 @@ const App = () => {
|
|
|
1076
1714
|
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1077
1715
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Press Esc or u to close..."))));
|
|
1078
1716
|
}
|
|
1717
|
+
if (showAddProjectModal) {
|
|
1718
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
|
|
1719
|
+
react_1.default.createElement(AddProjectModal, { onAdd: handleAddProject, onCancel: () => setShowAddProjectModal(false) })));
|
|
1720
|
+
}
|
|
1721
|
+
if (showConfirmDelete && selectedProject) {
|
|
1722
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
|
|
1723
|
+
react_1.default.createElement(ConfirmModal, { title: "Delete Project", message: `Are you sure you want to remove "${selectedProject.name}" from the dashboard?`, onConfirm: handleDeleteProject, onCancel: () => setShowConfirmDelete(false) })));
|
|
1724
|
+
}
|
|
1079
1725
|
if (showScriptModal && scriptModalData) {
|
|
1080
1726
|
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
|
|
1081
1727
|
react_1.default.createElement(ScriptSelectionModal, { scripts: scriptModalData.scripts, projectName: scriptModalData.projectName, projectPath: scriptModalData.projectPath, onSelect: handleScriptSelect, onClose: () => setShowScriptModal(false) })));
|
|
@@ -1094,11 +1740,133 @@ const App = () => {
|
|
|
1094
1740
|
}
|
|
1095
1741
|
}
|
|
1096
1742
|
};
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1743
|
+
// Render Projects view
|
|
1744
|
+
const renderProjectsView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "row", height: availableHeight, flexGrow: 0, flexShrink: 0 },
|
|
1745
|
+
react_1.default.createElement(ProjectListComponent, { projects: projects, selectedIndex: selectedIndex, runningProcesses: runningProcesses, isFocused: focusedPanel === 'list', height: availableHeight, scrollOffset: listScrollOffset, gitBranches: gitBranches, filterType: filterType, sortType: sortType }),
|
|
1746
|
+
react_1.default.createElement(ink_1.Box, { width: 1 }),
|
|
1747
|
+
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, gitBranch: selectedProject ? gitBranches.get(selectedProject.id) || null : null }),
|
|
1748
|
+
showTerminalPanel && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
1100
1749
|
react_1.default.createElement(ink_1.Box, { width: 1 }),
|
|
1101
|
-
react_1.default.createElement(
|
|
1750
|
+
react_1.default.createElement(TerminalOutputPanel, { processes: runningProcesses, selectedPid: selectedProcessPid, height: availableHeight, onSelectProcess: (pid) => setSelectedProcessPid(pid) })))));
|
|
1751
|
+
// Render Workspaces view
|
|
1752
|
+
const renderWorkspacesView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "row", height: availableHeight, flexGrow: 0, flexShrink: 0 },
|
|
1753
|
+
react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "35%", height: availableHeight, borderStyle: "round", borderColor: colors.accentCyan, padding: 1 },
|
|
1754
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.textPrimary },
|
|
1755
|
+
"Workspaces (",
|
|
1756
|
+
workspaces.length,
|
|
1757
|
+
")"),
|
|
1758
|
+
react_1.default.createElement(ink_1.Box, { flexDirection: "column", flexGrow: 1 }, workspaces.length === 0 ? (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "No workspaces found")) : (workspaces.map((ws, index) => {
|
|
1759
|
+
const isSelected = index === selectedWorkspaceIndex;
|
|
1760
|
+
return (react_1.default.createElement(ink_1.Text, { key: ws.id, color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
|
|
1761
|
+
isSelected ? '▶ ' : ' ',
|
|
1762
|
+
truncateText(ws.name, 25)));
|
|
1763
|
+
})))),
|
|
1764
|
+
react_1.default.createElement(ink_1.Box, { width: 1 }),
|
|
1765
|
+
react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "65%", height: availableHeight, borderStyle: "round", borderColor: colors.borderColor, padding: 1 }, workspaces[selectedWorkspaceIndex] ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
1766
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, workspaces[selectedWorkspaceIndex].name),
|
|
1767
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1768
|
+
workspaces[selectedWorkspaceIndex].description && (react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, workspaces[selectedWorkspaceIndex].description)),
|
|
1769
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1770
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
1771
|
+
"Path: ",
|
|
1772
|
+
getDisplayPath(workspaces[selectedWorkspaceIndex].workspace_file_path)))) : (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "Select a workspace")))));
|
|
1773
|
+
// Load workspaces from API
|
|
1774
|
+
const loadWorkspacesFromApi = async () => {
|
|
1775
|
+
try {
|
|
1776
|
+
// Try common API ports
|
|
1777
|
+
const ports = [38124, 38125, 38126, 38127, 38128, 3001];
|
|
1778
|
+
let apiBaseUrl = '';
|
|
1779
|
+
for (const port of ports) {
|
|
1780
|
+
try {
|
|
1781
|
+
const response = await fetch(`http://localhost:${port}/health`, {
|
|
1782
|
+
signal: AbortSignal.timeout(500),
|
|
1783
|
+
});
|
|
1784
|
+
if (response.ok) {
|
|
1785
|
+
apiBaseUrl = `http://localhost:${port}/api`;
|
|
1786
|
+
break;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
catch {
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
if (!apiBaseUrl) {
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
const response = await fetch(`${apiBaseUrl}/workspaces`);
|
|
1797
|
+
if (response.ok) {
|
|
1798
|
+
const ws = (await response.json());
|
|
1799
|
+
setWorkspaces(ws);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
catch {
|
|
1803
|
+
// Ignore workspace loading errors
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
// Render Processes view placeholder
|
|
1807
|
+
const renderProcessesView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 2 },
|
|
1808
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan },
|
|
1809
|
+
"Running Processes (",
|
|
1810
|
+
runningProcesses.length,
|
|
1811
|
+
")"),
|
|
1812
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1813
|
+
runningProcesses.length === 0 ? (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "No running processes")) : (runningProcesses.map((proc) => {
|
|
1814
|
+
const uptime = Math.floor((Date.now() - proc.startedAt) / 1000);
|
|
1815
|
+
const minutes = Math.floor(uptime / 60);
|
|
1816
|
+
const seconds = uptime % 60;
|
|
1817
|
+
const uptimeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
1818
|
+
return (react_1.default.createElement(ink_1.Text, { key: proc.pid, color: colors.textPrimary },
|
|
1819
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF"),
|
|
1820
|
+
" PID ",
|
|
1821
|
+
proc.pid,
|
|
1822
|
+
": ",
|
|
1823
|
+
proc.projectName,
|
|
1824
|
+
" (",
|
|
1825
|
+
proc.scriptName,
|
|
1826
|
+
") - ",
|
|
1827
|
+
uptimeStr));
|
|
1828
|
+
})),
|
|
1829
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1830
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "Press 1 to return to Projects")));
|
|
1831
|
+
// Render Settings view
|
|
1832
|
+
const renderSettingsView = () => {
|
|
1833
|
+
const currentOptions = settingsSection === 'editor' ? settingsEditorOptions : settingsBrowserOptions;
|
|
1834
|
+
const currentValue = settingsSection === 'editor' ? settings.editor.type : settings.browser.type;
|
|
1835
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 2 },
|
|
1836
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, "Settings"),
|
|
1837
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1838
|
+
react_1.default.createElement(ink_1.Box, null,
|
|
1839
|
+
react_1.default.createElement(ink_1.Text, { color: settingsSection === 'editor' ? colors.accentCyan : colors.textTertiary, bold: settingsSection === 'editor' }, "[Editor]"),
|
|
1840
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1841
|
+
react_1.default.createElement(ink_1.Text, { color: settingsSection === 'browser' ? colors.accentCyan : colors.textTertiary, bold: settingsSection === 'browser' }, "[Browser]")),
|
|
1842
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1843
|
+
currentOptions.map((option, index) => {
|
|
1844
|
+
const isSelected = index === settingsOptionIndex;
|
|
1845
|
+
const isActive = option === currentValue;
|
|
1846
|
+
return (react_1.default.createElement(ink_1.Text, { key: option, color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
|
|
1847
|
+
isSelected ? '▶ ' : ' ',
|
|
1848
|
+
isActive ? '● ' : '○ ',
|
|
1849
|
+
option.charAt(0).toUpperCase() + option.slice(1)));
|
|
1850
|
+
}),
|
|
1851
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1852
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "Tab: switch section | \u2191\u2193/jk: select | Space/Enter: choose")));
|
|
1853
|
+
};
|
|
1854
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", height: terminalHeight },
|
|
1855
|
+
react_1.default.createElement(ink_1.Box, { paddingX: 1, height: 1 },
|
|
1856
|
+
react_1.default.createElement(ink_1.Text, { color: currentView === 'projects' ? colors.accentCyan : colors.textTertiary }, "[1] Projects"),
|
|
1857
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1858
|
+
react_1.default.createElement(ink_1.Text, { color: currentView === 'workspaces' ? colors.accentCyan : colors.textTertiary }, "[2] Workspaces"),
|
|
1859
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1860
|
+
react_1.default.createElement(ink_1.Text, { color: currentView === 'processes' ? colors.accentCyan : colors.textTertiary }, "[3] Processes"),
|
|
1861
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1862
|
+
react_1.default.createElement(ink_1.Text, { color: currentView === 'settings' ? colors.accentCyan : colors.textTertiary }, "[4] Settings"),
|
|
1863
|
+
showTerminalPanel && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
1864
|
+
react_1.default.createElement(ink_1.Text, null, " | "),
|
|
1865
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "Terminal [T]")))),
|
|
1866
|
+
currentView === 'projects' && renderProjectsView(),
|
|
1867
|
+
currentView === 'workspaces' && renderWorkspacesView(),
|
|
1868
|
+
currentView === 'processes' && renderProcessesView(),
|
|
1869
|
+
currentView === 'settings' && renderSettingsView(),
|
|
1102
1870
|
react_1.default.createElement(ink_1.Box, { paddingX: 1, borderStyle: "single", borderColor: colors.borderColor, flexShrink: 0, height: 3 },
|
|
1103
1871
|
react_1.default.createElement(StatusBar, { focusedPanel: focusedPanel, selectedProject: selectedProject }))));
|
|
1104
1872
|
};
|