projax 3.3.58 → 3.3.59
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/dist/electron/script-runner.js +52 -20
- package/dist/index.js +1 -1
- package/dist/prxi.js +844 -109
- package/dist/prxi.tsx +1234 -179
- 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,264 @@ 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
|
+
};
|
|
308
|
+
const SettingsModal = ({ onClose }) => {
|
|
309
|
+
const [settings, setSettings] = (0, react_1.useState)({
|
|
310
|
+
editor: { type: 'vscode' },
|
|
311
|
+
browser: { type: 'chrome' },
|
|
312
|
+
});
|
|
313
|
+
const [selectedSection, setSelectedSection] = (0, react_1.useState)('editor');
|
|
314
|
+
const [selectedOptionIndex, setSelectedOptionIndex] = (0, react_1.useState)(0);
|
|
315
|
+
const editorOptions = [
|
|
316
|
+
'vscode',
|
|
317
|
+
'cursor',
|
|
318
|
+
'windsurf',
|
|
319
|
+
'zed',
|
|
320
|
+
'custom',
|
|
321
|
+
];
|
|
322
|
+
const browserOptions = [
|
|
323
|
+
'chrome',
|
|
324
|
+
'firefox',
|
|
325
|
+
'safari',
|
|
326
|
+
'edge',
|
|
327
|
+
'custom',
|
|
328
|
+
];
|
|
329
|
+
(0, react_1.useEffect)(() => {
|
|
330
|
+
// Load settings
|
|
331
|
+
try {
|
|
332
|
+
const settingsPath = path.join(os.homedir(), '.projax', 'settings.json');
|
|
333
|
+
if (fs.existsSync(settingsPath)) {
|
|
334
|
+
const data = fs.readFileSync(settingsPath, 'utf-8');
|
|
335
|
+
setSettings(JSON.parse(data));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// Use defaults
|
|
340
|
+
}
|
|
341
|
+
}, []);
|
|
342
|
+
const saveSettings = () => {
|
|
343
|
+
try {
|
|
344
|
+
const settingsDir = path.join(os.homedir(), '.projax');
|
|
345
|
+
if (!fs.existsSync(settingsDir)) {
|
|
346
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
347
|
+
}
|
|
348
|
+
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
349
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
350
|
+
onClose();
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Ignore save errors
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
(0, ink_1.useInput)((input, key) => {
|
|
357
|
+
if (key.escape || input === 'q') {
|
|
358
|
+
onClose();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (key.return) {
|
|
362
|
+
saveSettings();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (key.tab) {
|
|
366
|
+
setSelectedSection((prev) => (prev === 'editor' ? 'browser' : 'editor'));
|
|
367
|
+
setSelectedOptionIndex(0);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (key.upArrow || input === 'k') {
|
|
371
|
+
setSelectedOptionIndex((prev) => Math.max(0, prev - 1));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (key.downArrow || input === 'j') {
|
|
375
|
+
const maxIndex = selectedSection === 'editor' ? editorOptions.length - 1 : browserOptions.length - 1;
|
|
376
|
+
setSelectedOptionIndex((prev) => Math.min(maxIndex, prev + 1));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (input === ' ' || key.return) {
|
|
380
|
+
if (selectedSection === 'editor') {
|
|
381
|
+
setSettings({
|
|
382
|
+
...settings,
|
|
383
|
+
editor: { type: editorOptions[selectedOptionIndex] },
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
setSettings({
|
|
388
|
+
...settings,
|
|
389
|
+
browser: { type: browserOptions[selectedOptionIndex] },
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
const currentOptions = selectedSection === 'editor' ? editorOptions : browserOptions;
|
|
395
|
+
const currentValue = selectedSection === 'editor' ? settings.editor.type : settings.browser.type;
|
|
396
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentCyan, padding: 1, width: 60 },
|
|
397
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, "Settings"),
|
|
398
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
399
|
+
react_1.default.createElement(ink_1.Box, null,
|
|
400
|
+
react_1.default.createElement(ink_1.Text, { color: selectedSection === 'editor' ? colors.accentCyan : colors.textTertiary, bold: selectedSection === 'editor' }, "[Editor]"),
|
|
401
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
402
|
+
react_1.default.createElement(ink_1.Text, { color: selectedSection === 'browser' ? colors.accentCyan : colors.textTertiary, bold: selectedSection === 'browser' }, "[Browser]")),
|
|
403
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
404
|
+
currentOptions.map((option, index) => {
|
|
405
|
+
const isSelected = index === selectedOptionIndex;
|
|
406
|
+
const isActive = option === currentValue;
|
|
407
|
+
return (react_1.default.createElement(ink_1.Text, { key: option, color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
|
|
408
|
+
isSelected ? '▶ ' : ' ',
|
|
409
|
+
isActive ? '● ' : '○ ',
|
|
410
|
+
option.charAt(0).toUpperCase() + option.slice(1)));
|
|
411
|
+
}),
|
|
412
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
413
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Tab: switch section | \u2191\u2193: select | Space: choose | Enter: save | Esc: close")));
|
|
414
|
+
};
|
|
123
415
|
const ErrorModal = ({ message, onClose }) => {
|
|
124
416
|
(0, ink_1.useInput)((input, key) => {
|
|
125
417
|
if (key.escape || key.return) {
|
|
@@ -176,7 +468,7 @@ const ScriptSelectionModal = ({ scripts, projectName, projectPath, onSelect, onC
|
|
|
176
468
|
react_1.default.createElement(ink_1.Text, null, " "),
|
|
177
469
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "\u2191\u2193/kj: Navigate | Enter: Run | b: Background | Esc/q: Cancel")));
|
|
178
470
|
};
|
|
179
|
-
const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFocused, height, scrollOffset, }) => {
|
|
471
|
+
const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFocused, height, scrollOffset, gitBranches, filterType, sortType, }) => {
|
|
180
472
|
const { focus } = (0, ink_1.useFocus)({ id: 'projectList' });
|
|
181
473
|
// Calculate visible range
|
|
182
474
|
const startIndex = Math.max(0, scrollOffset);
|
|
@@ -189,10 +481,15 @@ const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFoc
|
|
|
189
481
|
const visibleProjects = projects.slice(startIndex, endIndex);
|
|
190
482
|
const hasMoreBelow = endIndex < projects.length;
|
|
191
483
|
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
|
-
|
|
484
|
+
react_1.default.createElement(ink_1.Box, { flexDirection: "column" },
|
|
485
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.textPrimary },
|
|
486
|
+
"Projects (",
|
|
487
|
+
projects.length,
|
|
488
|
+
")"),
|
|
489
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
490
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentPurple }, FILTER_LABELS[filterType]),
|
|
491
|
+
' | ',
|
|
492
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentOrange }, SORT_LABELS[sortType]))),
|
|
196
493
|
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
494
|
hasMoreAbove && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
198
495
|
"\u2191 ",
|
|
@@ -204,28 +501,37 @@ const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFoc
|
|
|
204
501
|
// Check if this project has running scripts
|
|
205
502
|
const projectRunning = runningProcesses.filter((p) => p.projectPath === project.path);
|
|
206
503
|
const hasRunningScripts = projectRunning.length > 0;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
504
|
+
// Get git branch
|
|
505
|
+
const branch = gitBranches.get(project.id);
|
|
506
|
+
const isMainBranch = branch === 'main' || branch === 'master';
|
|
507
|
+
return (react_1.default.createElement(ink_1.Box, { key: project.id, flexDirection: "column" },
|
|
508
|
+
react_1.default.createElement(ink_1.Text, { color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
|
|
509
|
+
isSelected ? '▶ ' : ' ',
|
|
510
|
+
hasRunningScripts && react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF "),
|
|
511
|
+
truncateText(project.name, 22),
|
|
512
|
+
hasRunningScripts && react_1.default.createElement(ink_1.Text, { color: colors.accentGreen },
|
|
513
|
+
" (",
|
|
514
|
+
projectRunning.length,
|
|
515
|
+
")")),
|
|
516
|
+
branch && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
517
|
+
' ',
|
|
518
|
+
react_1.default.createElement(ink_1.Text, { color: isMainBranch ? colors.accentGreen : colors.accentBlue }, truncateText(branch, 18))))));
|
|
215
519
|
}),
|
|
216
520
|
hasMoreBelow && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
217
521
|
"\u2193 ",
|
|
218
522
|
projects.length - endIndex,
|
|
219
523
|
" more below")))))));
|
|
220
524
|
};
|
|
221
|
-
const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editingName, editingDescription, editingTags, editInput, allTags, onTagRemove, height, scrollOffset, }) => {
|
|
525
|
+
const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editingName, editingDescription, editingTags, editInput, allTags, onTagRemove, height, scrollOffset, gitBranch, }) => {
|
|
222
526
|
const { focus } = (0, ink_1.useFocus)({ id: 'projectDetails' });
|
|
223
527
|
const [scripts, setScripts] = (0, react_1.useState)(null);
|
|
224
528
|
const [ports, setPorts] = (0, react_1.useState)([]);
|
|
529
|
+
const [npmPackage, setNpmPackage] = (0, react_1.useState)(null);
|
|
225
530
|
(0, react_1.useEffect)(() => {
|
|
226
531
|
if (!project) {
|
|
227
532
|
setScripts(null);
|
|
228
533
|
setPorts([]);
|
|
534
|
+
setNpmPackage(null);
|
|
229
535
|
return;
|
|
230
536
|
}
|
|
231
537
|
// Load scripts
|
|
@@ -245,6 +551,26 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
|
|
|
245
551
|
catch (error) {
|
|
246
552
|
setPorts([]);
|
|
247
553
|
}
|
|
554
|
+
// Check if npm package (live registry check)
|
|
555
|
+
setNpmPackage(null);
|
|
556
|
+
try {
|
|
557
|
+
const packageJsonPath = path.join(project.path, 'package.json');
|
|
558
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
559
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
560
|
+
if (pkg.name && !pkg.private) {
|
|
561
|
+
fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`, {
|
|
562
|
+
method: 'HEAD',
|
|
563
|
+
signal: AbortSignal.timeout(2000),
|
|
564
|
+
})
|
|
565
|
+
.then(res => {
|
|
566
|
+
if (res.ok)
|
|
567
|
+
setNpmPackage(pkg.name);
|
|
568
|
+
})
|
|
569
|
+
.catch(() => { });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch { }
|
|
248
574
|
}, [project]);
|
|
249
575
|
if (!project) {
|
|
250
576
|
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: isFocused ? colors.accentCyan : colors.borderColor, padding: 1 },
|
|
@@ -299,8 +625,20 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
|
|
|
299
625
|
react_1.default.createElement(ink_1.Text, null, " | "),
|
|
300
626
|
react_1.default.createElement(ink_1.Text, null,
|
|
301
627
|
"Scripts: ",
|
|
302
|
-
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts?.scripts?.size || 0))
|
|
628
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts?.scripts?.size || 0)),
|
|
629
|
+
npmPackage && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
630
|
+
react_1.default.createElement(ink_1.Text, null, " | "),
|
|
631
|
+
react_1.default.createElement(ink_1.Text, null,
|
|
632
|
+
"NPM: ",
|
|
633
|
+
react_1.default.createElement(ink_1.Text, { color: "#f85149" }, npmPackage))))));
|
|
303
634
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer2" }, " "));
|
|
635
|
+
// Git branch
|
|
636
|
+
if (gitBranch) {
|
|
637
|
+
const isMainBranch = gitBranch === 'main' || gitBranch === 'master';
|
|
638
|
+
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "git-branch" },
|
|
639
|
+
"Branch: ",
|
|
640
|
+
react_1.default.createElement(ink_1.Text, { color: isMainBranch ? colors.accentGreen : colors.accentBlue }, gitBranch)));
|
|
641
|
+
}
|
|
304
642
|
if (project.framework) {
|
|
305
643
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "framework" },
|
|
306
644
|
"Framework: ",
|
|
@@ -335,34 +673,28 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
|
|
|
335
673
|
});
|
|
336
674
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer4" }, " "));
|
|
337
675
|
}
|
|
338
|
-
// Scripts
|
|
676
|
+
// Scripts - show all, let virtual scrolling handle visibility
|
|
339
677
|
if (scripts && scripts.scripts && scripts.scripts.size > 0) {
|
|
340
678
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "scripts-header", bold: true },
|
|
341
679
|
"Available Scripts (",
|
|
342
680
|
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts.scripts.size),
|
|
343
681
|
"):"));
|
|
344
|
-
Array.from(scripts.scripts.entries()).
|
|
682
|
+
Array.from(scripts.scripts.entries()).forEach(([name, script]) => {
|
|
345
683
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: `script-${name}` },
|
|
346
684
|
' ',
|
|
347
685
|
react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, name),
|
|
348
686
|
' - ',
|
|
349
687
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, truncateText(script.command, 60))));
|
|
350
688
|
});
|
|
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
689
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer5" }, " "));
|
|
358
690
|
}
|
|
359
|
-
// Ports
|
|
691
|
+
// Ports - show all, let virtual scrolling handle visibility
|
|
360
692
|
if (ports.length > 0) {
|
|
361
693
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "ports-header", bold: true },
|
|
362
694
|
"Detected Ports (",
|
|
363
695
|
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, ports.length),
|
|
364
696
|
"):"));
|
|
365
|
-
ports.
|
|
697
|
+
ports.forEach((port) => {
|
|
366
698
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: `port-${port.id}` },
|
|
367
699
|
' ',
|
|
368
700
|
"Port ",
|
|
@@ -371,12 +703,6 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
|
|
|
371
703
|
" - ",
|
|
372
704
|
truncateText(port.config_source, 50))));
|
|
373
705
|
});
|
|
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
706
|
contentLines.push(react_1.default.createElement(ink_1.Text, { key: "spacer6" }, " "));
|
|
381
707
|
}
|
|
382
708
|
// Calculate visible range for virtual scrolling
|
|
@@ -404,6 +730,80 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
|
|
|
404
730
|
contentLines.length - endIndex,
|
|
405
731
|
" more below")))));
|
|
406
732
|
};
|
|
733
|
+
const TerminalOutputPanel = ({ processes, selectedPid, height, onSelectProcess, }) => {
|
|
734
|
+
const [logs, setLogs] = (0, react_1.useState)([]);
|
|
735
|
+
const [scrollOffset, setScrollOffset] = (0, react_1.useState)(0);
|
|
736
|
+
// Find the selected process or default to first
|
|
737
|
+
const activeProcess = selectedPid
|
|
738
|
+
? processes.find((p) => p.pid === selectedPid)
|
|
739
|
+
: processes[0];
|
|
740
|
+
(0, react_1.useEffect)(() => {
|
|
741
|
+
if (!activeProcess?.logFile) {
|
|
742
|
+
setLogs(['No active process selected']);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
// Read initial logs
|
|
746
|
+
try {
|
|
747
|
+
if (fs.existsSync(activeProcess.logFile)) {
|
|
748
|
+
const content = fs.readFileSync(activeProcess.logFile, 'utf-8');
|
|
749
|
+
const lines = content.split('\n').slice(-50); // Last 50 lines
|
|
750
|
+
setLogs(lines);
|
|
751
|
+
setScrollOffset(Math.max(0, lines.length - 10));
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
setLogs(['Log file not found']);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
setLogs(['Error reading logs']);
|
|
759
|
+
}
|
|
760
|
+
// Watch for changes
|
|
761
|
+
let watcher = null;
|
|
762
|
+
try {
|
|
763
|
+
watcher = fs.watch(activeProcess.logFile, () => {
|
|
764
|
+
try {
|
|
765
|
+
const content = fs.readFileSync(activeProcess.logFile, 'utf-8');
|
|
766
|
+
const lines = content.split('\n').slice(-100);
|
|
767
|
+
setLogs(lines);
|
|
768
|
+
// Auto-scroll to bottom
|
|
769
|
+
setScrollOffset(Math.max(0, lines.length - 10));
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
// Ignore read errors during watch
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
catch {
|
|
777
|
+
// Ignore watch errors
|
|
778
|
+
}
|
|
779
|
+
return () => {
|
|
780
|
+
if (watcher) {
|
|
781
|
+
watcher.close();
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}, [activeProcess?.logFile, activeProcess?.pid]);
|
|
785
|
+
const visibleLines = logs.slice(scrollOffset, scrollOffset + height - 4);
|
|
786
|
+
const hasMoreAbove = scrollOffset > 0;
|
|
787
|
+
const hasMoreBelow = scrollOffset + height - 4 < logs.length;
|
|
788
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "30%", height: height, borderStyle: "round", borderColor: colors.accentGreen, padding: 1, flexShrink: 0 },
|
|
789
|
+
react_1.default.createElement(ink_1.Box, { flexDirection: "row", justifyContent: "space-between" },
|
|
790
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentGreen }, "Terminal Output"),
|
|
791
|
+
processes.length > 1 && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
792
|
+
"[",
|
|
793
|
+
processes.findIndex((p) => p.pid === activeProcess?.pid) + 1,
|
|
794
|
+
"/",
|
|
795
|
+
processes.length,
|
|
796
|
+
"]"))),
|
|
797
|
+
activeProcess && (react_1.default.createElement(ink_1.Text, { color: colors.textSecondary },
|
|
798
|
+
truncateText(activeProcess.scriptName, 20),
|
|
799
|
+
" (PID: ",
|
|
800
|
+
activeProcess.pid,
|
|
801
|
+
")")),
|
|
802
|
+
react_1.default.createElement(ink_1.Box, { flexDirection: "column", flexGrow: 1, marginTop: 1 },
|
|
803
|
+
hasMoreAbove && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "\u2191 more above")),
|
|
804
|
+
visibleLines.map((line, idx) => (react_1.default.createElement(ink_1.Text, { key: idx, color: colors.textPrimary }, truncateText(line, 40)))),
|
|
805
|
+
hasMoreBelow && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "\u2193 more below")))));
|
|
806
|
+
};
|
|
407
807
|
const StatusBar = ({ focusedPanel, selectedProject }) => {
|
|
408
808
|
if (focusedPanel === 'list') {
|
|
409
809
|
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column" },
|
|
@@ -411,16 +811,22 @@ const StatusBar = ({ focusedPanel, selectedProject }) => {
|
|
|
411
811
|
react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF API"),
|
|
412
812
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " | "),
|
|
413
813
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Focus: "),
|
|
414
|
-
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "
|
|
814
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "List")),
|
|
415
815
|
react_1.default.createElement(ink_1.Box, null,
|
|
816
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "a"),
|
|
817
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Add | "),
|
|
416
818
|
react_1.default.createElement(ink_1.Text, { bold: true }, "/"),
|
|
417
819
|
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 }, "
|
|
820
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "F"),
|
|
821
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Filter | "),
|
|
822
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "S"),
|
|
823
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Sort | "),
|
|
824
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "\u2191\u2193"),
|
|
825
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Nav | "),
|
|
826
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "Tab"),
|
|
421
827
|
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 }, "
|
|
828
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "T"),
|
|
829
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Terminal | "),
|
|
424
830
|
react_1.default.createElement(ink_1.Text, { bold: true }, "?"),
|
|
425
831
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Help | "),
|
|
426
832
|
react_1.default.createElement(ink_1.Text, { bold: true }, "q"),
|
|
@@ -435,26 +841,18 @@ const StatusBar = ({ focusedPanel, selectedProject }) => {
|
|
|
435
841
|
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Details"),
|
|
436
842
|
selectedProject && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
437
843
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " | "),
|
|
438
|
-
react_1.default.createElement(ink_1.Text, { color: colors.textPrimary }, selectedProject.name)))),
|
|
844
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textPrimary }, truncateText(selectedProject.name, 20))))),
|
|
439
845
|
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
846
|
react_1.default.createElement(ink_1.Text, { bold: true }, "e"),
|
|
443
847
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Edit | "),
|
|
444
848
|
react_1.default.createElement(ink_1.Text, { bold: true }, "t"),
|
|
445
849
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Tags | "),
|
|
446
850
|
react_1.default.createElement(ink_1.Text, { bold: true }, "o"),
|
|
447
851
|
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 | "),
|
|
852
|
+
react_1.default.createElement(ink_1.Text, { bold: true }, "r"),
|
|
853
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Run | "),
|
|
452
854
|
react_1.default.createElement(ink_1.Text, { bold: true }, "s"),
|
|
453
855
|
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
856
|
react_1.default.createElement(ink_1.Text, { bold: true }, "x"),
|
|
459
857
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Stop | "),
|
|
460
858
|
react_1.default.createElement(ink_1.Text, { bold: true }, "d"),
|
|
@@ -462,9 +860,7 @@ const StatusBar = ({ focusedPanel, selectedProject }) => {
|
|
|
462
860
|
react_1.default.createElement(ink_1.Text, { bold: true }, "Tab"),
|
|
463
861
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Switch | "),
|
|
464
862
|
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"))));
|
|
863
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Help"))));
|
|
468
864
|
};
|
|
469
865
|
// Simple fuzzy search function
|
|
470
866
|
function fuzzyMatch(query, text) {
|
|
@@ -492,6 +888,13 @@ const App = () => {
|
|
|
492
888
|
const [error, setError] = (0, react_1.useState)(null);
|
|
493
889
|
const [runningProcesses, setRunningProcesses] = (0, react_1.useState)([]);
|
|
494
890
|
const [focusedPanel, setFocusedPanel] = (0, react_1.useState)('list');
|
|
891
|
+
// View state
|
|
892
|
+
const [currentView, setCurrentView] = (0, react_1.useState)('projects');
|
|
893
|
+
// Git branches
|
|
894
|
+
const [gitBranches, setGitBranches] = (0, react_1.useState)(new Map());
|
|
895
|
+
// Filter and sort state
|
|
896
|
+
const [filterType, setFilterType] = (0, react_1.useState)('all');
|
|
897
|
+
const [sortType, setSortType] = (0, react_1.useState)('name-asc');
|
|
495
898
|
// Editing state
|
|
496
899
|
const [editingName, setEditingName] = (0, react_1.useState)(false);
|
|
497
900
|
const [editingDescription, setEditingDescription] = (0, react_1.useState)(false);
|
|
@@ -499,6 +902,9 @@ const App = () => {
|
|
|
499
902
|
const [editInput, setEditInput] = (0, react_1.useState)('');
|
|
500
903
|
const [showUrls, setShowUrls] = (0, react_1.useState)(false);
|
|
501
904
|
const [allTags, setAllTags] = (0, react_1.useState)([]);
|
|
905
|
+
// Modal state
|
|
906
|
+
const [showAddProjectModal, setShowAddProjectModal] = (0, react_1.useState)(false);
|
|
907
|
+
const [showConfirmDelete, setShowConfirmDelete] = (0, react_1.useState)(false);
|
|
502
908
|
// Search state
|
|
503
909
|
const [showSearch, setShowSearch] = (0, react_1.useState)(false);
|
|
504
910
|
const [searchQuery, setSearchQuery] = (0, react_1.useState)('');
|
|
@@ -507,19 +913,47 @@ const App = () => {
|
|
|
507
913
|
// Script selection state
|
|
508
914
|
const [showScriptModal, setShowScriptModal] = (0, react_1.useState)(false);
|
|
509
915
|
const [scriptModalData, setScriptModalData] = (0, react_1.useState)(null);
|
|
916
|
+
// Workspace state
|
|
917
|
+
const [workspaces, setWorkspaces] = (0, react_1.useState)([]);
|
|
918
|
+
const [selectedWorkspaceIndex, setSelectedWorkspaceIndex] = (0, react_1.useState)(0);
|
|
919
|
+
// Terminal panel state
|
|
920
|
+
const [showTerminalPanel, setShowTerminalPanel] = (0, react_1.useState)(false);
|
|
921
|
+
const [terminalLogs, setTerminalLogs] = (0, react_1.useState)([]);
|
|
922
|
+
const [selectedProcessPid, setSelectedProcessPid] = (0, react_1.useState)(null);
|
|
923
|
+
// Settings state
|
|
924
|
+
const [showSettings, setShowSettings] = (0, react_1.useState)(false);
|
|
510
925
|
// Get terminal dimensions
|
|
511
926
|
const terminalHeight = process.stdout.rows || 24;
|
|
512
|
-
const availableHeight = terminalHeight -
|
|
927
|
+
const availableHeight = terminalHeight - 4; // Subtract status bar (increased for view indicator)
|
|
513
928
|
(0, react_1.useEffect)(() => {
|
|
514
929
|
loadProjects();
|
|
515
930
|
loadRunningProcesses();
|
|
516
931
|
loadAllTags();
|
|
517
|
-
// Refresh running processes every 5 seconds
|
|
932
|
+
// Refresh running processes and git branches every 5 seconds
|
|
518
933
|
const interval = setInterval(() => {
|
|
519
934
|
loadRunningProcesses();
|
|
520
935
|
}, 5000);
|
|
521
936
|
return () => clearInterval(interval);
|
|
522
937
|
}, []);
|
|
938
|
+
// Load git branches when projects change
|
|
939
|
+
(0, react_1.useEffect)(() => {
|
|
940
|
+
if (allProjects.length > 0) {
|
|
941
|
+
loadGitBranches();
|
|
942
|
+
}
|
|
943
|
+
}, [allProjects]);
|
|
944
|
+
const loadGitBranches = async () => {
|
|
945
|
+
const branches = new Map();
|
|
946
|
+
for (const project of allProjects) {
|
|
947
|
+
try {
|
|
948
|
+
const branch = (0, core_bridge_1.getCurrentBranch)(project.path);
|
|
949
|
+
branches.set(project.id, branch);
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
branches.set(project.id, null);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
setGitBranches(branches);
|
|
956
|
+
};
|
|
523
957
|
// Reset editing state and scroll when project changes
|
|
524
958
|
(0, react_1.useEffect)(() => {
|
|
525
959
|
setEditingName(false);
|
|
@@ -528,6 +962,12 @@ const App = () => {
|
|
|
528
962
|
setEditInput('');
|
|
529
963
|
setDetailsScrollOffset(0); // Reset scroll when switching projects
|
|
530
964
|
}, [selectedIndex]);
|
|
965
|
+
// Load workspaces when switching to workspaces view
|
|
966
|
+
(0, react_1.useEffect)(() => {
|
|
967
|
+
if (currentView === 'workspaces' && workspaces.length === 0) {
|
|
968
|
+
loadWorkspacesFromApi();
|
|
969
|
+
}
|
|
970
|
+
}, [currentView]);
|
|
531
971
|
// Update scroll offset when selected index changes
|
|
532
972
|
(0, react_1.useEffect)(() => {
|
|
533
973
|
const visibleHeight = Math.max(1, availableHeight - 3);
|
|
@@ -560,26 +1000,71 @@ const App = () => {
|
|
|
560
1000
|
const loadProjects = () => {
|
|
561
1001
|
const loadedProjects = (0, core_bridge_1.getAllProjects)();
|
|
562
1002
|
setAllProjects(loadedProjects);
|
|
563
|
-
|
|
1003
|
+
applyFilterAndSort(loadedProjects, searchQuery, filterType, sortType);
|
|
564
1004
|
};
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
1005
|
+
const applyFilterAndSort = (projectsToFilter, query, filter, sort) => {
|
|
1006
|
+
let filtered = projectsToFilter;
|
|
1007
|
+
// Apply search query with filter type
|
|
1008
|
+
if (query.trim()) {
|
|
1009
|
+
const q = query.toLowerCase();
|
|
1010
|
+
filtered = projectsToFilter.filter(project => {
|
|
1011
|
+
switch (filter) {
|
|
1012
|
+
case 'name':
|
|
1013
|
+
return fuzzyMatch(q, project.name);
|
|
1014
|
+
case 'path':
|
|
1015
|
+
return fuzzyMatch(q, project.path);
|
|
1016
|
+
case 'tags':
|
|
1017
|
+
return project.tags?.some((tag) => fuzzyMatch(q, tag)) || false;
|
|
1018
|
+
case 'ports': {
|
|
1019
|
+
// Check if project has ports matching the query
|
|
1020
|
+
const db = (0, core_bridge_1.getDatabaseManager)();
|
|
1021
|
+
const ports = db.getProjectPorts(project.id);
|
|
1022
|
+
return ports.some((p) => p.port.toString().includes(q));
|
|
1023
|
+
}
|
|
1024
|
+
case 'running': {
|
|
1025
|
+
const isRunning = runningProcesses.some((p) => p.projectPath === project.path);
|
|
1026
|
+
return (q === 'running' || q === 'yes' || q === 'true') ? isRunning : !isRunning;
|
|
1027
|
+
}
|
|
1028
|
+
case 'all':
|
|
1029
|
+
default:
|
|
1030
|
+
return (fuzzyMatch(q, project.name) ||
|
|
1031
|
+
(project.description ? fuzzyMatch(q, project.description) : false) ||
|
|
1032
|
+
fuzzyMatch(q, project.path) ||
|
|
1033
|
+
project.tags?.some((tag) => fuzzyMatch(q, tag)) ||
|
|
1034
|
+
false);
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
569
1037
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
1038
|
+
// Apply sorting
|
|
1039
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
1040
|
+
switch (sort) {
|
|
1041
|
+
case 'name-asc':
|
|
1042
|
+
return a.name.localeCompare(b.name);
|
|
1043
|
+
case 'name-desc':
|
|
1044
|
+
return b.name.localeCompare(a.name);
|
|
1045
|
+
case 'recent':
|
|
1046
|
+
return (b.last_scanned || 0) - (a.last_scanned || 0);
|
|
1047
|
+
case 'oldest':
|
|
1048
|
+
return (a.created_at || 0) - (b.created_at || 0);
|
|
1049
|
+
case 'running': {
|
|
1050
|
+
const aRunning = runningProcesses.filter((p) => p.projectPath === a.path).length;
|
|
1051
|
+
const bRunning = runningProcesses.filter((p) => p.projectPath === b.path).length;
|
|
1052
|
+
return bRunning - aRunning;
|
|
1053
|
+
}
|
|
1054
|
+
default:
|
|
1055
|
+
return 0;
|
|
1056
|
+
}
|
|
576
1057
|
});
|
|
577
|
-
setProjects(
|
|
1058
|
+
setProjects(sorted);
|
|
578
1059
|
// Adjust selected index if current selection is out of bounds
|
|
579
|
-
if (selectedIndex >=
|
|
580
|
-
setSelectedIndex(Math.max(0,
|
|
1060
|
+
if (selectedIndex >= sorted.length) {
|
|
1061
|
+
setSelectedIndex(Math.max(0, sorted.length - 1));
|
|
581
1062
|
}
|
|
582
1063
|
};
|
|
1064
|
+
// Re-apply filter/sort when dependencies change
|
|
1065
|
+
(0, react_1.useEffect)(() => {
|
|
1066
|
+
applyFilterAndSort(allProjects, searchQuery, filterType, sortType);
|
|
1067
|
+
}, [filterType, sortType, runningProcesses]);
|
|
583
1068
|
const loadRunningProcesses = async () => {
|
|
584
1069
|
try {
|
|
585
1070
|
const processes = await (0, script_runner_1.getRunningProcessesClean)();
|
|
@@ -726,13 +1211,83 @@ const App = () => {
|
|
|
726
1211
|
setError(err instanceof Error ? err.message : String(err));
|
|
727
1212
|
}
|
|
728
1213
|
};
|
|
1214
|
+
// Handler for adding a project
|
|
1215
|
+
const handleAddProject = async (projectPath, projectName) => {
|
|
1216
|
+
setShowAddProjectModal(false);
|
|
1217
|
+
setIsLoading(true);
|
|
1218
|
+
setLoadingMessage('Adding project...');
|
|
1219
|
+
try {
|
|
1220
|
+
const name = projectName || path.basename(projectPath);
|
|
1221
|
+
const db = (0, core_bridge_1.getDatabaseManager)();
|
|
1222
|
+
const project = db.addProject(name, projectPath);
|
|
1223
|
+
// Scan for tests
|
|
1224
|
+
setLoadingMessage('Scanning for tests...');
|
|
1225
|
+
await (0, core_bridge_1.scanProject)(project.id);
|
|
1226
|
+
// Scan for ports
|
|
1227
|
+
setLoadingMessage('Scanning for ports...');
|
|
1228
|
+
try {
|
|
1229
|
+
const { scanProjectPorts } = await Promise.resolve().then(() => __importStar(require('./port-scanner')));
|
|
1230
|
+
await scanProjectPorts(project.id);
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
// Ignore port scanning errors
|
|
1234
|
+
}
|
|
1235
|
+
loadProjects();
|
|
1236
|
+
setIsLoading(false);
|
|
1237
|
+
// Select the newly added project
|
|
1238
|
+
const newProjects = (0, core_bridge_1.getAllProjects)();
|
|
1239
|
+
const newIndex = newProjects.findIndex((p) => p.id === project.id);
|
|
1240
|
+
if (newIndex >= 0) {
|
|
1241
|
+
setSelectedIndex(newIndex);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
catch (err) {
|
|
1245
|
+
setIsLoading(false);
|
|
1246
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
// Handler for deleting a project
|
|
1250
|
+
const handleDeleteProject = () => {
|
|
1251
|
+
if (!selectedProject)
|
|
1252
|
+
return;
|
|
1253
|
+
setShowConfirmDelete(false);
|
|
1254
|
+
setIsLoading(true);
|
|
1255
|
+
setLoadingMessage(`Deleting ${selectedProject.name}...`);
|
|
1256
|
+
setTimeout(async () => {
|
|
1257
|
+
try {
|
|
1258
|
+
const db = (0, core_bridge_1.getDatabaseManager)();
|
|
1259
|
+
db.removeProject(selectedProject.id);
|
|
1260
|
+
loadProjects();
|
|
1261
|
+
if (selectedIndex >= projects.length - 1) {
|
|
1262
|
+
setSelectedIndex(Math.max(0, projects.length - 2));
|
|
1263
|
+
}
|
|
1264
|
+
setIsLoading(false);
|
|
1265
|
+
}
|
|
1266
|
+
catch (err) {
|
|
1267
|
+
setIsLoading(false);
|
|
1268
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1269
|
+
}
|
|
1270
|
+
}, 100);
|
|
1271
|
+
};
|
|
1272
|
+
// Cycle filter type
|
|
1273
|
+
const cycleFilterType = () => {
|
|
1274
|
+
const currentIndex = FILTER_TYPES.indexOf(filterType);
|
|
1275
|
+
const nextIndex = (currentIndex + 1) % FILTER_TYPES.length;
|
|
1276
|
+
setFilterType(FILTER_TYPES[nextIndex]);
|
|
1277
|
+
};
|
|
1278
|
+
// Cycle sort type
|
|
1279
|
+
const cycleSortType = () => {
|
|
1280
|
+
const currentIndex = SORT_TYPES.indexOf(sortType);
|
|
1281
|
+
const nextIndex = (currentIndex + 1) % SORT_TYPES.length;
|
|
1282
|
+
setSortType(SORT_TYPES[nextIndex]);
|
|
1283
|
+
};
|
|
729
1284
|
(0, ink_1.useInput)((input, key) => {
|
|
730
1285
|
// Handle search mode
|
|
731
1286
|
if (showSearch) {
|
|
732
1287
|
if (key.escape) {
|
|
733
1288
|
setShowSearch(false);
|
|
734
1289
|
setSearchQuery('');
|
|
735
|
-
|
|
1290
|
+
applyFilterAndSort(allProjects, '', filterType, sortType);
|
|
736
1291
|
return;
|
|
737
1292
|
}
|
|
738
1293
|
if (key.return) {
|
|
@@ -742,19 +1297,19 @@ const App = () => {
|
|
|
742
1297
|
if (key.backspace || key.delete) {
|
|
743
1298
|
const newQuery = searchQuery.slice(0, -1);
|
|
744
1299
|
setSearchQuery(newQuery);
|
|
745
|
-
|
|
1300
|
+
applyFilterAndSort(allProjects, newQuery, filterType, sortType);
|
|
746
1301
|
return;
|
|
747
1302
|
}
|
|
748
1303
|
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
749
1304
|
const newQuery = searchQuery + input;
|
|
750
1305
|
setSearchQuery(newQuery);
|
|
751
|
-
|
|
1306
|
+
applyFilterAndSort(allProjects, newQuery, filterType, sortType);
|
|
752
1307
|
return;
|
|
753
1308
|
}
|
|
754
1309
|
return;
|
|
755
1310
|
}
|
|
756
1311
|
// Don't process input if modal is showing
|
|
757
|
-
if (showHelp || isLoading || error || showUrls || showScriptModal) {
|
|
1312
|
+
if (showHelp || isLoading || error || showUrls || showScriptModal || showAddProjectModal || showConfirmDelete || showSettings) {
|
|
758
1313
|
// Handle URLs modal
|
|
759
1314
|
if (showUrls && (key.escape || key.return || input === 'q' || input === 'u')) {
|
|
760
1315
|
setShowUrls(false);
|
|
@@ -762,12 +1317,95 @@ const App = () => {
|
|
|
762
1317
|
}
|
|
763
1318
|
return;
|
|
764
1319
|
}
|
|
1320
|
+
// Handle navigation in workspaces view
|
|
1321
|
+
if (currentView === 'workspaces') {
|
|
1322
|
+
if (key.upArrow || input === 'k') {
|
|
1323
|
+
setSelectedWorkspaceIndex((prev) => Math.max(0, prev - 1));
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
if (key.downArrow || input === 'j') {
|
|
1327
|
+
setSelectedWorkspaceIndex((prev) => Math.min(workspaces.length - 1, prev + 1));
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
// Handle navigation in processes view
|
|
1332
|
+
if (currentView === 'processes') {
|
|
1333
|
+
if (input === 'x' && runningProcesses.length > 0) {
|
|
1334
|
+
// Stop all processes (or could select one)
|
|
1335
|
+
setIsLoading(true);
|
|
1336
|
+
setLoadingMessage('Stopping processes...');
|
|
1337
|
+
setTimeout(async () => {
|
|
1338
|
+
try {
|
|
1339
|
+
for (const proc of runningProcesses) {
|
|
1340
|
+
await (0, script_runner_1.stopScript)(proc.pid);
|
|
1341
|
+
}
|
|
1342
|
+
await loadRunningProcesses();
|
|
1343
|
+
setIsLoading(false);
|
|
1344
|
+
}
|
|
1345
|
+
catch (err) {
|
|
1346
|
+
setIsLoading(false);
|
|
1347
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1348
|
+
}
|
|
1349
|
+
}, 100);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
// Global navigation - number keys for view switching
|
|
1354
|
+
if (input === '1') {
|
|
1355
|
+
setCurrentView('projects');
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
if (input === '2') {
|
|
1359
|
+
setCurrentView('workspaces');
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (input === '3') {
|
|
1363
|
+
setCurrentView('processes');
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (input === '4') {
|
|
1367
|
+
setCurrentView('settings');
|
|
1368
|
+
setShowSettings(true);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
// Terminal panel toggle
|
|
1372
|
+
if (input === 'T') {
|
|
1373
|
+
setShowTerminalPanel(prev => !prev);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
765
1376
|
// Search shortcut
|
|
766
1377
|
if (input === '/') {
|
|
767
1378
|
setShowSearch(true);
|
|
768
1379
|
setSearchQuery('');
|
|
769
1380
|
return;
|
|
770
1381
|
}
|
|
1382
|
+
// Add project shortcut (in projects view)
|
|
1383
|
+
if (input === 'a' && currentView === 'projects') {
|
|
1384
|
+
setShowAddProjectModal(true);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
// Filter cycle shortcut
|
|
1388
|
+
if (input === 'F' && currentView === 'projects') {
|
|
1389
|
+
cycleFilterType();
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
// Sort cycle shortcut
|
|
1393
|
+
if (input === 'S' && currentView === 'projects') {
|
|
1394
|
+
cycleSortType();
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
// Refresh git branches
|
|
1398
|
+
if (input === 'g' && currentView === 'projects') {
|
|
1399
|
+
loadGitBranches();
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
// Full refresh
|
|
1403
|
+
if (input === 'R') {
|
|
1404
|
+
loadProjects();
|
|
1405
|
+
loadRunningProcesses();
|
|
1406
|
+
loadGitBranches();
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
771
1409
|
// Handle editing modes
|
|
772
1410
|
if (editingName || editingDescription || editingTags) {
|
|
773
1411
|
if (key.escape) {
|
|
@@ -924,25 +1562,9 @@ const App = () => {
|
|
|
924
1562
|
setShowUrls(true);
|
|
925
1563
|
return;
|
|
926
1564
|
}
|
|
927
|
-
// Delete project
|
|
1565
|
+
// Delete project (with confirmation)
|
|
928
1566
|
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);
|
|
1567
|
+
setShowConfirmDelete(true);
|
|
946
1568
|
return;
|
|
947
1569
|
}
|
|
948
1570
|
}
|
|
@@ -1032,6 +1654,13 @@ const App = () => {
|
|
|
1032
1654
|
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
|
|
1033
1655
|
react_1.default.createElement(HelpModal, { onClose: () => setShowHelp(false) })));
|
|
1034
1656
|
}
|
|
1657
|
+
if (showSettings) {
|
|
1658
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
|
|
1659
|
+
react_1.default.createElement(SettingsModal, { onClose: () => {
|
|
1660
|
+
setShowSettings(false);
|
|
1661
|
+
setCurrentView('projects');
|
|
1662
|
+
} })));
|
|
1663
|
+
}
|
|
1035
1664
|
if (isLoading) {
|
|
1036
1665
|
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
|
|
1037
1666
|
react_1.default.createElement(LoadingModal, { message: loadingMessage })));
|
|
@@ -1076,6 +1705,14 @@ const App = () => {
|
|
|
1076
1705
|
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1077
1706
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Press Esc or u to close..."))));
|
|
1078
1707
|
}
|
|
1708
|
+
if (showAddProjectModal) {
|
|
1709
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
|
|
1710
|
+
react_1.default.createElement(AddProjectModal, { onAdd: handleAddProject, onCancel: () => setShowAddProjectModal(false) })));
|
|
1711
|
+
}
|
|
1712
|
+
if (showConfirmDelete && selectedProject) {
|
|
1713
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
|
|
1714
|
+
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) })));
|
|
1715
|
+
}
|
|
1079
1716
|
if (showScriptModal && scriptModalData) {
|
|
1080
1717
|
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 1 },
|
|
1081
1718
|
react_1.default.createElement(ScriptSelectionModal, { scripts: scriptModalData.scripts, projectName: scriptModalData.projectName, projectPath: scriptModalData.projectPath, onSelect: handleScriptSelect, onClose: () => setShowScriptModal(false) })));
|
|
@@ -1094,11 +1731,109 @@ const App = () => {
|
|
|
1094
1731
|
}
|
|
1095
1732
|
}
|
|
1096
1733
|
};
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1734
|
+
// Render Projects view
|
|
1735
|
+
const renderProjectsView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "row", height: availableHeight, flexGrow: 0, flexShrink: 0 },
|
|
1736
|
+
react_1.default.createElement(ProjectListComponent, { projects: projects, selectedIndex: selectedIndex, runningProcesses: runningProcesses, isFocused: focusedPanel === 'list', height: availableHeight, scrollOffset: listScrollOffset, gitBranches: gitBranches, filterType: filterType, sortType: sortType }),
|
|
1737
|
+
react_1.default.createElement(ink_1.Box, { width: 1 }),
|
|
1738
|
+
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 }),
|
|
1739
|
+
showTerminalPanel && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
1100
1740
|
react_1.default.createElement(ink_1.Box, { width: 1 }),
|
|
1101
|
-
react_1.default.createElement(
|
|
1741
|
+
react_1.default.createElement(TerminalOutputPanel, { processes: runningProcesses, selectedPid: selectedProcessPid, height: availableHeight, onSelectProcess: (pid) => setSelectedProcessPid(pid) })))));
|
|
1742
|
+
// Render Workspaces view
|
|
1743
|
+
const renderWorkspacesView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "row", height: availableHeight, flexGrow: 0, flexShrink: 0 },
|
|
1744
|
+
react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "35%", height: availableHeight, borderStyle: "round", borderColor: colors.accentCyan, padding: 1 },
|
|
1745
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.textPrimary },
|
|
1746
|
+
"Workspaces (",
|
|
1747
|
+
workspaces.length,
|
|
1748
|
+
")"),
|
|
1749
|
+
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) => {
|
|
1750
|
+
const isSelected = index === selectedWorkspaceIndex;
|
|
1751
|
+
return (react_1.default.createElement(ink_1.Text, { key: ws.id, color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
|
|
1752
|
+
isSelected ? '▶ ' : ' ',
|
|
1753
|
+
truncateText(ws.name, 25)));
|
|
1754
|
+
})))),
|
|
1755
|
+
react_1.default.createElement(ink_1.Box, { width: 1 }),
|
|
1756
|
+
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,
|
|
1757
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, workspaces[selectedWorkspaceIndex].name),
|
|
1758
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1759
|
+
workspaces[selectedWorkspaceIndex].description && (react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, workspaces[selectedWorkspaceIndex].description)),
|
|
1760
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1761
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
1762
|
+
"Path: ",
|
|
1763
|
+
getDisplayPath(workspaces[selectedWorkspaceIndex].workspace_file_path)))) : (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "Select a workspace")))));
|
|
1764
|
+
// Load workspaces from API
|
|
1765
|
+
const loadWorkspacesFromApi = async () => {
|
|
1766
|
+
try {
|
|
1767
|
+
// Try common API ports
|
|
1768
|
+
const ports = [38124, 38125, 38126, 38127, 38128, 3001];
|
|
1769
|
+
let apiBaseUrl = '';
|
|
1770
|
+
for (const port of ports) {
|
|
1771
|
+
try {
|
|
1772
|
+
const response = await fetch(`http://localhost:${port}/health`, {
|
|
1773
|
+
signal: AbortSignal.timeout(500),
|
|
1774
|
+
});
|
|
1775
|
+
if (response.ok) {
|
|
1776
|
+
apiBaseUrl = `http://localhost:${port}/api`;
|
|
1777
|
+
break;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
catch {
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (!apiBaseUrl) {
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
const response = await fetch(`${apiBaseUrl}/workspaces`);
|
|
1788
|
+
if (response.ok) {
|
|
1789
|
+
const ws = (await response.json());
|
|
1790
|
+
setWorkspaces(ws);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
catch {
|
|
1794
|
+
// Ignore workspace loading errors
|
|
1795
|
+
}
|
|
1796
|
+
};
|
|
1797
|
+
// Render Processes view placeholder
|
|
1798
|
+
const renderProcessesView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 2 },
|
|
1799
|
+
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan },
|
|
1800
|
+
"Running Processes (",
|
|
1801
|
+
runningProcesses.length,
|
|
1802
|
+
")"),
|
|
1803
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1804
|
+
runningProcesses.length === 0 ? (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "No running processes")) : (runningProcesses.map((proc) => {
|
|
1805
|
+
const uptime = Math.floor((Date.now() - proc.startedAt) / 1000);
|
|
1806
|
+
const minutes = Math.floor(uptime / 60);
|
|
1807
|
+
const seconds = uptime % 60;
|
|
1808
|
+
const uptimeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
1809
|
+
return (react_1.default.createElement(ink_1.Text, { key: proc.pid, color: colors.textPrimary },
|
|
1810
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF"),
|
|
1811
|
+
" PID ",
|
|
1812
|
+
proc.pid,
|
|
1813
|
+
": ",
|
|
1814
|
+
proc.projectName,
|
|
1815
|
+
" (",
|
|
1816
|
+
proc.scriptName,
|
|
1817
|
+
") - ",
|
|
1818
|
+
uptimeStr));
|
|
1819
|
+
})),
|
|
1820
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1821
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "Press 1 to return to Projects")));
|
|
1822
|
+
return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", height: terminalHeight },
|
|
1823
|
+
react_1.default.createElement(ink_1.Box, { paddingX: 1, height: 1 },
|
|
1824
|
+
react_1.default.createElement(ink_1.Text, { color: currentView === 'projects' ? colors.accentCyan : colors.textTertiary }, "[1] Projects"),
|
|
1825
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1826
|
+
react_1.default.createElement(ink_1.Text, { color: currentView === 'workspaces' ? colors.accentCyan : colors.textTertiary }, "[2] Workspaces"),
|
|
1827
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1828
|
+
react_1.default.createElement(ink_1.Text, { color: currentView === 'processes' ? colors.accentCyan : colors.textTertiary }, "[3] Processes"),
|
|
1829
|
+
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1830
|
+
react_1.default.createElement(ink_1.Text, { color: currentView === 'settings' ? colors.accentCyan : colors.textTertiary }, "[4] Settings"),
|
|
1831
|
+
showTerminalPanel && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
1832
|
+
react_1.default.createElement(ink_1.Text, null, " | "),
|
|
1833
|
+
react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "Terminal [T]")))),
|
|
1834
|
+
currentView === 'projects' && renderProjectsView(),
|
|
1835
|
+
currentView === 'workspaces' && renderWorkspacesView(),
|
|
1836
|
+
currentView === 'processes' && renderProcessesView(),
|
|
1102
1837
|
react_1.default.createElement(ink_1.Box, { paddingX: 1, borderStyle: "single", borderColor: colors.borderColor, flexShrink: 0, height: 3 },
|
|
1103
1838
|
react_1.default.createElement(StatusBar, { focusedPanel: focusedPanel, selectedProject: selectedProject }))));
|
|
1104
1839
|
};
|