projax 3.3.66 → 3.3.68
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/prxi.js +111 -22
- package/dist/prxi.tsx +108 -19
- package/package.json +19 -20
package/dist/prxi.js
CHANGED
|
@@ -139,6 +139,7 @@ const HelpModal = ({ onClose }) => {
|
|
|
139
139
|
react_1.default.createElement(ink_1.Text, null, " u Show detected URLs"),
|
|
140
140
|
react_1.default.createElement(ink_1.Text, null, " s Scan project for tests"),
|
|
141
141
|
react_1.default.createElement(ink_1.Text, null, " p Scan ports for project"),
|
|
142
|
+
react_1.default.createElement(ink_1.Text, null, " Enter Run selected script (in details panel)"),
|
|
142
143
|
react_1.default.createElement(ink_1.Text, null, " r Run scripts (select from list)"),
|
|
143
144
|
react_1.default.createElement(ink_1.Text, null, " x Stop all scripts for project"),
|
|
144
145
|
react_1.default.createElement(ink_1.Text, null, " d Delete project (with confirmation)"),
|
|
@@ -336,12 +337,12 @@ const ScriptSelectionModal = ({ scripts, projectName, projectPath, onSelect, onC
|
|
|
336
337
|
}
|
|
337
338
|
if (key.return) {
|
|
338
339
|
const [scriptName] = scriptArray[selectedIndex];
|
|
339
|
-
onSelect(scriptName,
|
|
340
|
+
onSelect(scriptName, true);
|
|
340
341
|
return;
|
|
341
342
|
}
|
|
342
|
-
if (input === '
|
|
343
|
+
if (input === 'f') {
|
|
343
344
|
const [scriptName] = scriptArray[selectedIndex];
|
|
344
|
-
onSelect(scriptName,
|
|
345
|
+
onSelect(scriptName, false);
|
|
345
346
|
return;
|
|
346
347
|
}
|
|
347
348
|
});
|
|
@@ -359,7 +360,7 @@ const ScriptSelectionModal = ({ scripts, projectName, projectPath, onSelect, onC
|
|
|
359
360
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, truncateText(script.command, 50))));
|
|
360
361
|
}),
|
|
361
362
|
react_1.default.createElement(ink_1.Text, null, " "),
|
|
362
|
-
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "\u2191\u2193/kj: Navigate | Enter: Run |
|
|
363
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "\u2191\u2193/kj: Navigate | Enter: Run (bg) | f: Foreground | Esc/q: Cancel")));
|
|
363
364
|
};
|
|
364
365
|
const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFocused, height, scrollOffset, gitBranches, filterType, sortType, }) => {
|
|
365
366
|
const { focus } = (0, ink_1.useFocus)({ id: 'projectList' });
|
|
@@ -415,7 +416,7 @@ const ProjectListComponent = ({ projects, selectedIndex, runningProcesses, isFoc
|
|
|
415
416
|
projects.length - endIndex,
|
|
416
417
|
" more below")))))));
|
|
417
418
|
};
|
|
418
|
-
const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editingName, editingDescription, editingTags, editInput, allTags, onTagRemove, height, scrollOffset, gitBranch, }) => {
|
|
419
|
+
const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editingName, editingDescription, editingTags, editInput, allTags, onTagRemove, height, scrollOffset, gitBranch, selectedScriptIndex, }) => {
|
|
419
420
|
const { focus } = (0, ink_1.useFocus)({ id: 'projectDetails' });
|
|
420
421
|
const [scripts, setScripts] = (0, react_1.useState)(null);
|
|
421
422
|
const [ports, setPorts] = (0, react_1.useState)([]);
|
|
@@ -572,10 +573,11 @@ const ProjectDetailsComponent = ({ project, runningProcesses, isFocused, editing
|
|
|
572
573
|
"Available Scripts (",
|
|
573
574
|
react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, scripts.scripts.size),
|
|
574
575
|
"):"));
|
|
575
|
-
Array.from(scripts.scripts.entries()).forEach(([name, script]) => {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
576
|
+
Array.from(scripts.scripts.entries()).forEach(([name, script], idx) => {
|
|
577
|
+
const isScriptSelected = isFocused && idx === selectedScriptIndex;
|
|
578
|
+
contentLines.push(react_1.default.createElement(ink_1.Text, { key: `script-${name}`, bold: isScriptSelected },
|
|
579
|
+
isScriptSelected ? '▶ ' : ' ',
|
|
580
|
+
react_1.default.createElement(ink_1.Text, { color: isScriptSelected ? colors.accentCyan : colors.accentGreen }, name),
|
|
579
581
|
' - ',
|
|
580
582
|
react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, truncateText(script.command, 60))));
|
|
581
583
|
});
|
|
@@ -806,6 +808,7 @@ const App = () => {
|
|
|
806
808
|
// Script selection state
|
|
807
809
|
const [showScriptModal, setShowScriptModal] = (0, react_1.useState)(false);
|
|
808
810
|
const [scriptModalData, setScriptModalData] = (0, react_1.useState)(null);
|
|
811
|
+
const [detailSelectedScript, setDetailSelectedScript] = (0, react_1.useState)(0);
|
|
809
812
|
// Workspace state
|
|
810
813
|
const [workspaces, setWorkspaces] = (0, react_1.useState)([]);
|
|
811
814
|
const [selectedWorkspaceIndex, setSelectedWorkspaceIndex] = (0, react_1.useState)(0);
|
|
@@ -813,6 +816,7 @@ const App = () => {
|
|
|
813
816
|
const [showTerminalPanel, setShowTerminalPanel] = (0, react_1.useState)(false);
|
|
814
817
|
const [terminalLogs, setTerminalLogs] = (0, react_1.useState)([]);
|
|
815
818
|
const [selectedProcessPid, setSelectedProcessPid] = (0, react_1.useState)(null);
|
|
819
|
+
const [selectedProcessIndex, setSelectedProcessIndex] = (0, react_1.useState)(0);
|
|
816
820
|
// Settings state
|
|
817
821
|
const [settings, setSettings] = (0, react_1.useState)({
|
|
818
822
|
editor: { type: 'vscode' },
|
|
@@ -876,6 +880,7 @@ const App = () => {
|
|
|
876
880
|
setEditingTags(false);
|
|
877
881
|
setEditInput('');
|
|
878
882
|
setDetailsScrollOffset(0); // Reset scroll when switching projects
|
|
883
|
+
setDetailSelectedScript(0); // Reset selected script
|
|
879
884
|
}, [selectedIndex]);
|
|
880
885
|
// Load workspaces when switching to workspaces view
|
|
881
886
|
(0, react_1.useEffect)(() => {
|
|
@@ -980,6 +985,15 @@ const App = () => {
|
|
|
980
985
|
(0, react_1.useEffect)(() => {
|
|
981
986
|
applyFilterAndSort(allProjects, searchQuery, filterType, sortType);
|
|
982
987
|
}, [filterType, sortType, runningProcesses]);
|
|
988
|
+
// Reset/clamp selectedProcessIndex when processes change
|
|
989
|
+
(0, react_1.useEffect)(() => {
|
|
990
|
+
if (runningProcesses.length === 0) {
|
|
991
|
+
setSelectedProcessIndex(0);
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
setSelectedProcessIndex((prev) => Math.min(prev, runningProcesses.length - 1));
|
|
995
|
+
}
|
|
996
|
+
}, [runningProcesses.length]);
|
|
983
997
|
const loadRunningProcesses = async () => {
|
|
984
998
|
try {
|
|
985
999
|
const processes = await (0, script_runner_1.getRunningProcessesClean)();
|
|
@@ -1306,10 +1320,38 @@ const App = () => {
|
|
|
1306
1320
|
}
|
|
1307
1321
|
// Handle navigation in processes view
|
|
1308
1322
|
if (currentView === 'processes') {
|
|
1323
|
+
if (key.upArrow || input === 'k') {
|
|
1324
|
+
setSelectedProcessIndex((prev) => Math.max(0, prev - 1));
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (key.downArrow || input === 'j') {
|
|
1328
|
+
setSelectedProcessIndex((prev) => Math.min(runningProcesses.length - 1, prev + 1));
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1309
1331
|
if (input === 'x' && runningProcesses.length > 0) {
|
|
1310
|
-
// Stop
|
|
1332
|
+
// Stop the selected process
|
|
1333
|
+
const selectedProc = runningProcesses[selectedProcessIndex];
|
|
1334
|
+
if (!selectedProc)
|
|
1335
|
+
return;
|
|
1311
1336
|
setIsLoading(true);
|
|
1312
|
-
setLoadingMessage(
|
|
1337
|
+
setLoadingMessage(`Stopping process ${selectedProc.pid}...`);
|
|
1338
|
+
setTimeout(async () => {
|
|
1339
|
+
try {
|
|
1340
|
+
await (0, script_runner_1.stopScript)(selectedProc.pid);
|
|
1341
|
+
await loadRunningProcesses();
|
|
1342
|
+
setIsLoading(false);
|
|
1343
|
+
}
|
|
1344
|
+
catch (err) {
|
|
1345
|
+
setIsLoading(false);
|
|
1346
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1347
|
+
}
|
|
1348
|
+
}, 100);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (input === 'X' && runningProcesses.length > 0) {
|
|
1352
|
+
// Stop ALL processes
|
|
1353
|
+
setIsLoading(true);
|
|
1354
|
+
setLoadingMessage('Stopping all processes...');
|
|
1313
1355
|
setTimeout(async () => {
|
|
1314
1356
|
try {
|
|
1315
1357
|
for (const proc of runningProcesses) {
|
|
@@ -1536,15 +1578,54 @@ const App = () => {
|
|
|
1536
1578
|
}
|
|
1537
1579
|
// Details panel actions
|
|
1538
1580
|
if (focusedPanel === 'details' && selectedProject) {
|
|
1539
|
-
//
|
|
1581
|
+
// Navigate scripts in details panel
|
|
1540
1582
|
if (key.upArrow || input === 'k') {
|
|
1541
|
-
|
|
1583
|
+
const projectScripts = (0, script_runner_1.getProjectScripts)(selectedProject.path);
|
|
1584
|
+
if (projectScripts.scripts.size > 0) {
|
|
1585
|
+
setDetailSelectedScript(prev => Math.max(0, prev - 1));
|
|
1586
|
+
}
|
|
1587
|
+
else {
|
|
1588
|
+
setDetailsScrollOffset(prev => Math.max(0, prev - 1));
|
|
1589
|
+
}
|
|
1542
1590
|
return;
|
|
1543
1591
|
}
|
|
1544
1592
|
if (key.downArrow || input === 'j') {
|
|
1545
|
-
const
|
|
1546
|
-
|
|
1547
|
-
|
|
1593
|
+
const projectScripts = (0, script_runner_1.getProjectScripts)(selectedProject.path);
|
|
1594
|
+
if (projectScripts.scripts.size > 0) {
|
|
1595
|
+
setDetailSelectedScript(prev => Math.min(projectScripts.scripts.size - 1, prev + 1));
|
|
1596
|
+
}
|
|
1597
|
+
else {
|
|
1598
|
+
setDetailsScrollOffset(prev => prev + 1);
|
|
1599
|
+
}
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
// Run selected script with Enter
|
|
1603
|
+
if (key.return) {
|
|
1604
|
+
try {
|
|
1605
|
+
const projectScripts = (0, script_runner_1.getProjectScripts)(selectedProject.path);
|
|
1606
|
+
if (projectScripts.scripts.size > 0) {
|
|
1607
|
+
const scriptArray = Array.from(projectScripts.scripts.keys());
|
|
1608
|
+
const scriptName = scriptArray[detailSelectedScript];
|
|
1609
|
+
if (scriptName) {
|
|
1610
|
+
setIsLoading(true);
|
|
1611
|
+
setLoadingMessage(`Running ${scriptName} in background...`);
|
|
1612
|
+
setTimeout(async () => {
|
|
1613
|
+
try {
|
|
1614
|
+
await (0, script_runner_1.runScriptInBackground)(selectedProject.path, selectedProject.name, scriptName, [], false);
|
|
1615
|
+
setIsLoading(false);
|
|
1616
|
+
await loadRunningProcesses();
|
|
1617
|
+
}
|
|
1618
|
+
catch (err) {
|
|
1619
|
+
setIsLoading(false);
|
|
1620
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1621
|
+
}
|
|
1622
|
+
}, 100);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
catch (err) {
|
|
1627
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1628
|
+
}
|
|
1548
1629
|
return;
|
|
1549
1630
|
}
|
|
1550
1631
|
// Edit name
|
|
@@ -1744,7 +1825,7 @@ const App = () => {
|
|
|
1744
1825
|
const renderProjectsView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "row", height: availableHeight, flexGrow: 0, flexShrink: 0 },
|
|
1745
1826
|
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
1827
|
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 }),
|
|
1828
|
+
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, selectedScriptIndex: detailSelectedScript }),
|
|
1748
1829
|
showTerminalPanel && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
1749
1830
|
react_1.default.createElement(ink_1.Box, { width: 1 }),
|
|
1750
1831
|
react_1.default.createElement(TerminalOutputPanel, { processes: runningProcesses, selectedPid: selectedProcessPid, height: availableHeight, onSelectProcess: (pid) => setSelectedProcessPid(pid) })))));
|
|
@@ -1803,19 +1884,24 @@ const App = () => {
|
|
|
1803
1884
|
// Ignore workspace loading errors
|
|
1804
1885
|
}
|
|
1805
1886
|
};
|
|
1806
|
-
// Render Processes view
|
|
1887
|
+
// Render Processes view
|
|
1807
1888
|
const renderProcessesView = () => (react_1.default.createElement(ink_1.Box, { flexDirection: "column", padding: 2 },
|
|
1808
1889
|
react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan },
|
|
1809
1890
|
"Running Processes (",
|
|
1810
1891
|
runningProcesses.length,
|
|
1811
1892
|
")"),
|
|
1812
1893
|
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) => {
|
|
1894
|
+
runningProcesses.length === 0 ? (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "No running processes")) : (runningProcesses.map((proc, index) => {
|
|
1814
1895
|
const uptime = Math.floor((Date.now() - proc.startedAt) / 1000);
|
|
1815
1896
|
const minutes = Math.floor(uptime / 60);
|
|
1816
1897
|
const seconds = uptime % 60;
|
|
1817
1898
|
const uptimeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
1818
|
-
|
|
1899
|
+
const isSelected = index === selectedProcessIndex;
|
|
1900
|
+
const portsStr = proc.detectedPorts && proc.detectedPorts.length > 0
|
|
1901
|
+
? ` [ports: ${proc.detectedPorts.join(', ')}]`
|
|
1902
|
+
: '';
|
|
1903
|
+
return (react_1.default.createElement(ink_1.Text, { key: proc.pid, color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
|
|
1904
|
+
isSelected ? '> ' : ' ',
|
|
1819
1905
|
react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF"),
|
|
1820
1906
|
" PID ",
|
|
1821
1907
|
proc.pid,
|
|
@@ -1824,10 +1910,13 @@ const App = () => {
|
|
|
1824
1910
|
" (",
|
|
1825
1911
|
proc.scriptName,
|
|
1826
1912
|
") - ",
|
|
1827
|
-
uptimeStr
|
|
1913
|
+
uptimeStr,
|
|
1914
|
+
portsStr && react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, portsStr)));
|
|
1828
1915
|
})),
|
|
1829
1916
|
react_1.default.createElement(ink_1.Text, null, " "),
|
|
1830
|
-
react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
|
|
1917
|
+
react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, runningProcesses.length > 0
|
|
1918
|
+
? 'up/down: navigate | x: stop selected | X: stop all | 1: back to projects'
|
|
1919
|
+
: 'Press 1 to return to Projects')));
|
|
1831
1920
|
// Render Settings view
|
|
1832
1921
|
const renderSettingsView = () => {
|
|
1833
1922
|
const currentOptions = settingsSection === 'editor' ? settingsEditorOptions : settingsBrowserOptions;
|
package/dist/prxi.tsx
CHANGED
|
@@ -162,6 +162,7 @@ const HelpModal: React.FC<HelpModalProps> = ({ onClose }) => {
|
|
|
162
162
|
<Text> u Show detected URLs</Text>
|
|
163
163
|
<Text> s Scan project for tests</Text>
|
|
164
164
|
<Text> p Scan ports for project</Text>
|
|
165
|
+
<Text> Enter Run selected script (in details panel)</Text>
|
|
165
166
|
<Text> r Run scripts (select from list)</Text>
|
|
166
167
|
<Text> x Stop all scripts for project</Text>
|
|
167
168
|
<Text> d Delete project (with confirmation)</Text>
|
|
@@ -470,13 +471,13 @@ const ScriptSelectionModal: React.FC<ScriptSelectionModalProps> = ({
|
|
|
470
471
|
|
|
471
472
|
if (key.return) {
|
|
472
473
|
const [scriptName] = scriptArray[selectedIndex];
|
|
473
|
-
onSelect(scriptName,
|
|
474
|
+
onSelect(scriptName, true);
|
|
474
475
|
return;
|
|
475
476
|
}
|
|
476
477
|
|
|
477
|
-
if (input === '
|
|
478
|
+
if (input === 'f') {
|
|
478
479
|
const [scriptName] = scriptArray[selectedIndex];
|
|
479
|
-
onSelect(scriptName,
|
|
480
|
+
onSelect(scriptName, false);
|
|
480
481
|
return;
|
|
481
482
|
}
|
|
482
483
|
});
|
|
@@ -505,7 +506,7 @@ const ScriptSelectionModal: React.FC<ScriptSelectionModalProps> = ({
|
|
|
505
506
|
);
|
|
506
507
|
})}
|
|
507
508
|
<Text> </Text>
|
|
508
|
-
<Text color={colors.textSecondary}>↑↓/kj: Navigate | Enter: Run |
|
|
509
|
+
<Text color={colors.textSecondary}>↑↓/kj: Navigate | Enter: Run (bg) | f: Foreground | Esc/q: Cancel</Text>
|
|
509
510
|
</Box>
|
|
510
511
|
);
|
|
511
512
|
};
|
|
@@ -631,6 +632,7 @@ interface ProjectDetailsProps {
|
|
|
631
632
|
height: number;
|
|
632
633
|
scrollOffset: number;
|
|
633
634
|
gitBranch: string | null;
|
|
635
|
+
selectedScriptIndex: number;
|
|
634
636
|
}
|
|
635
637
|
|
|
636
638
|
const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
@@ -646,6 +648,7 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
|
646
648
|
height,
|
|
647
649
|
scrollOffset,
|
|
648
650
|
gitBranch,
|
|
651
|
+
selectedScriptIndex,
|
|
649
652
|
}) => {
|
|
650
653
|
const { focus } = useFocus({ id: 'projectDetails' });
|
|
651
654
|
const [scripts, setScripts] = useState<any>(null);
|
|
@@ -858,11 +861,12 @@ const ProjectDetailsComponent: React.FC<ProjectDetailsProps> = ({
|
|
|
858
861
|
Available Scripts (<Text color={colors.accentCyan}>{scripts.scripts.size}</Text>):
|
|
859
862
|
</Text>
|
|
860
863
|
);
|
|
861
|
-
Array.from(scripts.scripts.entries() as IterableIterator<[string, any]>).forEach(([name, script]) => {
|
|
864
|
+
Array.from(scripts.scripts.entries() as IterableIterator<[string, any]>).forEach(([name, script], idx) => {
|
|
865
|
+
const isScriptSelected = isFocused && idx === selectedScriptIndex;
|
|
862
866
|
contentLines.push(
|
|
863
|
-
<Text key={`script-${name}`}>
|
|
864
|
-
{' '}
|
|
865
|
-
<Text color={colors.accentGreen}>{name}</Text>
|
|
867
|
+
<Text key={`script-${name}`} bold={isScriptSelected}>
|
|
868
|
+
{isScriptSelected ? '▶ ' : ' '}
|
|
869
|
+
<Text color={isScriptSelected ? colors.accentCyan : colors.accentGreen}>{name}</Text>
|
|
866
870
|
{' - '}
|
|
867
871
|
<Text color={colors.textSecondary}>{truncateText(script.command, 60)}</Text>
|
|
868
872
|
</Text>
|
|
@@ -1177,6 +1181,7 @@ const App: React.FC = () => {
|
|
|
1177
1181
|
// Script selection state
|
|
1178
1182
|
const [showScriptModal, setShowScriptModal] = useState(false);
|
|
1179
1183
|
const [scriptModalData, setScriptModalData] = useState<{ scripts: Map<string, any>; projectName: string; projectPath: string } | null>(null);
|
|
1184
|
+
const [detailSelectedScript, setDetailSelectedScript] = useState(0);
|
|
1180
1185
|
|
|
1181
1186
|
// Workspace state
|
|
1182
1187
|
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
|
@@ -1186,6 +1191,7 @@ const App: React.FC = () => {
|
|
|
1186
1191
|
const [showTerminalPanel, setShowTerminalPanel] = useState(false);
|
|
1187
1192
|
const [terminalLogs, setTerminalLogs] = useState<string[]>([]);
|
|
1188
1193
|
const [selectedProcessPid, setSelectedProcessPid] = useState<number | null>(null);
|
|
1194
|
+
const [selectedProcessIndex, setSelectedProcessIndex] = useState(0);
|
|
1189
1195
|
|
|
1190
1196
|
// Settings state
|
|
1191
1197
|
const [settings, setSettings] = useState<AppSettings>({
|
|
@@ -1256,6 +1262,7 @@ const App: React.FC = () => {
|
|
|
1256
1262
|
setEditingTags(false);
|
|
1257
1263
|
setEditInput('');
|
|
1258
1264
|
setDetailsScrollOffset(0); // Reset scroll when switching projects
|
|
1265
|
+
setDetailSelectedScript(0); // Reset selected script
|
|
1259
1266
|
}, [selectedIndex]);
|
|
1260
1267
|
|
|
1261
1268
|
// Load workspaces when switching to workspaces view
|
|
@@ -1376,6 +1383,15 @@ const App: React.FC = () => {
|
|
|
1376
1383
|
applyFilterAndSort(allProjects, searchQuery, filterType, sortType);
|
|
1377
1384
|
}, [filterType, sortType, runningProcesses]);
|
|
1378
1385
|
|
|
1386
|
+
// Reset/clamp selectedProcessIndex when processes change
|
|
1387
|
+
useEffect(() => {
|
|
1388
|
+
if (runningProcesses.length === 0) {
|
|
1389
|
+
setSelectedProcessIndex(0);
|
|
1390
|
+
} else {
|
|
1391
|
+
setSelectedProcessIndex((prev) => Math.min(prev, runningProcesses.length - 1));
|
|
1392
|
+
}
|
|
1393
|
+
}, [runningProcesses.length]);
|
|
1394
|
+
|
|
1379
1395
|
const loadRunningProcesses = async () => {
|
|
1380
1396
|
try {
|
|
1381
1397
|
const processes = await getRunningProcessesClean();
|
|
@@ -1727,10 +1743,36 @@ const App: React.FC = () => {
|
|
|
1727
1743
|
|
|
1728
1744
|
// Handle navigation in processes view
|
|
1729
1745
|
if (currentView === 'processes') {
|
|
1746
|
+
if (key.upArrow || input === 'k') {
|
|
1747
|
+
setSelectedProcessIndex((prev) => Math.max(0, prev - 1));
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
if (key.downArrow || input === 'j') {
|
|
1751
|
+
setSelectedProcessIndex((prev) => Math.min(runningProcesses.length - 1, prev + 1));
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1730
1754
|
if (input === 'x' && runningProcesses.length > 0) {
|
|
1731
|
-
// Stop
|
|
1755
|
+
// Stop the selected process
|
|
1756
|
+
const selectedProc = runningProcesses[selectedProcessIndex];
|
|
1757
|
+
if (!selectedProc) return;
|
|
1732
1758
|
setIsLoading(true);
|
|
1733
|
-
setLoadingMessage(
|
|
1759
|
+
setLoadingMessage(`Stopping process ${selectedProc.pid}...`);
|
|
1760
|
+
setTimeout(async () => {
|
|
1761
|
+
try {
|
|
1762
|
+
await stopScript(selectedProc.pid);
|
|
1763
|
+
await loadRunningProcesses();
|
|
1764
|
+
setIsLoading(false);
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
setIsLoading(false);
|
|
1767
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1768
|
+
}
|
|
1769
|
+
}, 100);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
if (input === 'X' && runningProcesses.length > 0) {
|
|
1773
|
+
// Stop ALL processes
|
|
1774
|
+
setIsLoading(true);
|
|
1775
|
+
setLoadingMessage('Stopping all processes...');
|
|
1734
1776
|
setTimeout(async () => {
|
|
1735
1777
|
try {
|
|
1736
1778
|
for (const proc of runningProcesses) {
|
|
@@ -1969,16 +2011,52 @@ const App: React.FC = () => {
|
|
|
1969
2011
|
|
|
1970
2012
|
// Details panel actions
|
|
1971
2013
|
if (focusedPanel === 'details' && selectedProject) {
|
|
1972
|
-
//
|
|
2014
|
+
// Navigate scripts in details panel
|
|
1973
2015
|
if (key.upArrow || input === 'k') {
|
|
1974
|
-
|
|
2016
|
+
const projectScripts = getProjectScripts(selectedProject.path);
|
|
2017
|
+
if (projectScripts.scripts.size > 0) {
|
|
2018
|
+
setDetailSelectedScript(prev => Math.max(0, prev - 1));
|
|
2019
|
+
} else {
|
|
2020
|
+
setDetailsScrollOffset(prev => Math.max(0, prev - 1));
|
|
2021
|
+
}
|
|
1975
2022
|
return;
|
|
1976
2023
|
}
|
|
1977
2024
|
|
|
1978
2025
|
if (key.downArrow || input === 'j') {
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
2026
|
+
const projectScripts = getProjectScripts(selectedProject.path);
|
|
2027
|
+
if (projectScripts.scripts.size > 0) {
|
|
2028
|
+
setDetailSelectedScript(prev => Math.min(projectScripts.scripts.size - 1, prev + 1));
|
|
2029
|
+
} else {
|
|
2030
|
+
setDetailsScrollOffset(prev => prev + 1);
|
|
2031
|
+
}
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// Run selected script with Enter
|
|
2036
|
+
if (key.return) {
|
|
2037
|
+
try {
|
|
2038
|
+
const projectScripts = getProjectScripts(selectedProject.path);
|
|
2039
|
+
if (projectScripts.scripts.size > 0) {
|
|
2040
|
+
const scriptArray = Array.from(projectScripts.scripts.keys());
|
|
2041
|
+
const scriptName = scriptArray[detailSelectedScript];
|
|
2042
|
+
if (scriptName) {
|
|
2043
|
+
setIsLoading(true);
|
|
2044
|
+
setLoadingMessage(`Running ${scriptName} in background...`);
|
|
2045
|
+
setTimeout(async () => {
|
|
2046
|
+
try {
|
|
2047
|
+
await runScriptInBackground(selectedProject.path, selectedProject.name, scriptName, [], false);
|
|
2048
|
+
setIsLoading(false);
|
|
2049
|
+
await loadRunningProcesses();
|
|
2050
|
+
} catch (err) {
|
|
2051
|
+
setIsLoading(false);
|
|
2052
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2053
|
+
}
|
|
2054
|
+
}, 100);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2059
|
+
}
|
|
1982
2060
|
return;
|
|
1983
2061
|
}
|
|
1984
2062
|
|
|
@@ -2283,6 +2361,7 @@ const App: React.FC = () => {
|
|
|
2283
2361
|
height={availableHeight}
|
|
2284
2362
|
scrollOffset={detailsScrollOffset}
|
|
2285
2363
|
gitBranch={selectedProject ? gitBranches.get(selectedProject.id) || null : null}
|
|
2364
|
+
selectedScriptIndex={detailSelectedScript}
|
|
2286
2365
|
/>
|
|
2287
2366
|
{showTerminalPanel && (
|
|
2288
2367
|
<>
|
|
@@ -2396,7 +2475,7 @@ const App: React.FC = () => {
|
|
|
2396
2475
|
}
|
|
2397
2476
|
};
|
|
2398
2477
|
|
|
2399
|
-
// Render Processes view
|
|
2478
|
+
// Render Processes view
|
|
2400
2479
|
const renderProcessesView = () => (
|
|
2401
2480
|
<Box flexDirection="column" padding={2}>
|
|
2402
2481
|
<Text bold color={colors.accentCyan}>Running Processes ({runningProcesses.length})</Text>
|
|
@@ -2404,20 +2483,30 @@ const App: React.FC = () => {
|
|
|
2404
2483
|
{runningProcesses.length === 0 ? (
|
|
2405
2484
|
<Text color={colors.textTertiary}>No running processes</Text>
|
|
2406
2485
|
) : (
|
|
2407
|
-
runningProcesses.map((proc: any) => {
|
|
2486
|
+
runningProcesses.map((proc: any, index: number) => {
|
|
2408
2487
|
const uptime = Math.floor((Date.now() - proc.startedAt) / 1000);
|
|
2409
2488
|
const minutes = Math.floor(uptime / 60);
|
|
2410
2489
|
const seconds = uptime % 60;
|
|
2411
2490
|
const uptimeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
2491
|
+
const isSelected = index === selectedProcessIndex;
|
|
2492
|
+
const portsStr = proc.detectedPorts && proc.detectedPorts.length > 0
|
|
2493
|
+
? ` [ports: ${proc.detectedPorts.join(', ')}]`
|
|
2494
|
+
: '';
|
|
2412
2495
|
return (
|
|
2413
|
-
<Text key={proc.pid} color={colors.textPrimary}>
|
|
2496
|
+
<Text key={proc.pid} color={isSelected ? colors.accentCyan : colors.textPrimary} bold={isSelected}>
|
|
2497
|
+
{isSelected ? '> ' : ' '}
|
|
2414
2498
|
<Text color={colors.accentGreen}>●</Text> PID {proc.pid}: {proc.projectName} ({proc.scriptName}) - {uptimeStr}
|
|
2499
|
+
{portsStr && <Text color={colors.accentCyan}>{portsStr}</Text>}
|
|
2415
2500
|
</Text>
|
|
2416
2501
|
);
|
|
2417
2502
|
})
|
|
2418
2503
|
)}
|
|
2419
2504
|
<Text> </Text>
|
|
2420
|
-
<Text color={colors.textTertiary}>
|
|
2505
|
+
<Text color={colors.textTertiary}>
|
|
2506
|
+
{runningProcesses.length > 0
|
|
2507
|
+
? 'up/down: navigate | x: stop selected | X: stop all | 1: back to projects'
|
|
2508
|
+
: 'Press 1 to return to Projects'}
|
|
2509
|
+
</Text>
|
|
2421
2510
|
</Box>
|
|
2422
2511
|
);
|
|
2423
2512
|
|
package/package.json
CHANGED
|
@@ -1,28 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projax",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.68",
|
|
4
4
|
"description": "Cross-platform project management dashboard for tracking local development projects. Features CLI, Terminal UI, Desktop app, REST API, and built-in tools for test detection, port management, and script execution.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"prx": "./dist/index.js"
|
|
8
8
|
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "tsc",
|
|
11
|
-
"typecheck": "echo \"Skipping cli typecheck (legacy)\"",
|
|
12
|
-
"lint": "echo \"Skipping cli lint (not configured)\"",
|
|
13
|
-
"lint:fix": "echo \"Skipping cli lint (not configured)\"",
|
|
14
|
-
"build:electron": "cd ../desktop && pnpm run build",
|
|
15
|
-
"copy:electron": "mkdir -p dist/electron && cp -r ../desktop/dist/* dist/electron/ && cp dist/script-runner.* dist/electron/ && cp dist/port-*.* dist/electron/ && cp -R dist/core dist/electron/",
|
|
16
|
-
"copy:api": "mkdir -p dist/api && cp -r ../api/dist/* dist/api/ && cp ../api/package.json dist/api/",
|
|
17
|
-
"copy:core": "mkdir -p dist/core && cp -r ../core/dist/* dist/core/",
|
|
18
|
-
"copy:prxi": "cp src/prxi.tsx dist/prxi.tsx",
|
|
19
|
-
"build:all": "pnpm run build && pnpm run copy:core && pnpm run build:electron && pnpm run copy:electron && pnpm run copy:api && pnpm run copy:prxi",
|
|
20
|
-
"clean": "rm -rf dist",
|
|
21
|
-
"test": "jest",
|
|
22
|
-
"test:watch": "jest --watch",
|
|
23
|
-
"test:coverage": "jest --coverage",
|
|
24
|
-
"prepublishOnly": "pnpm run build:all"
|
|
25
|
-
},
|
|
26
9
|
"dependencies": {
|
|
27
10
|
"chokidar": "^3.6.0",
|
|
28
11
|
"commander": "^11.1.0",
|
|
@@ -74,5 +57,21 @@
|
|
|
74
57
|
},
|
|
75
58
|
"homepage": "https://projax.dev",
|
|
76
59
|
"author": "",
|
|
77
|
-
"license": "MIT"
|
|
78
|
-
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsc",
|
|
63
|
+
"typecheck": "echo \"Skipping cli typecheck (legacy)\"",
|
|
64
|
+
"lint": "echo \"Skipping cli lint (not configured)\"",
|
|
65
|
+
"lint:fix": "echo \"Skipping cli lint (not configured)\"",
|
|
66
|
+
"build:electron": "cd ../desktop && pnpm run build",
|
|
67
|
+
"copy:electron": "mkdir -p dist/electron && cp -r ../desktop/dist/* dist/electron/ && cp dist/script-runner.* dist/electron/ && cp dist/port-*.* dist/electron/ && cp -R dist/core dist/electron/",
|
|
68
|
+
"copy:api": "mkdir -p dist/api && cp -r ../api/dist/* dist/api/ && cp ../api/package.json dist/api/",
|
|
69
|
+
"copy:core": "mkdir -p dist/core && cp -r ../core/dist/* dist/core/",
|
|
70
|
+
"copy:prxi": "cp src/prxi.tsx dist/prxi.tsx",
|
|
71
|
+
"build:all": "pnpm run build && pnpm run copy:core && pnpm run build:electron && pnpm run copy:electron && pnpm run copy:api && pnpm run copy:prxi",
|
|
72
|
+
"clean": "rm -rf dist",
|
|
73
|
+
"test": "jest",
|
|
74
|
+
"test:watch": "jest --watch",
|
|
75
|
+
"test:coverage": "jest --coverage"
|
|
76
|
+
}
|
|
77
|
+
}
|