project-compass 4.2.0 โ 4.3.0
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/package.json +28 -3
- package/src/cli.js +91 -21
- package/src/components/Footer.js +64 -8
- package/src/components/Header.js +47 -8
- package/src/components/Navigator.js +69 -15
- package/src/detectors/dotnet.js +110 -5
- package/src/detectors/frameworks.js +692 -42
- package/src/detectors/go.js +111 -10
- package/src/detectors/java.js +129 -14
- package/src/detectors/node.js +69 -11
- package/src/detectors/php.js +98 -5
- package/src/detectors/python.js +137 -39
- package/src/detectors/ruby.js +105 -5
- package/src/detectors/rust.js +111 -10
- package/src/detectors/utils.js +95 -7
- package/memory/2026-03-02.md +0 -28
- package/memory/2026-03-03.md +0 -22
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-compass",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "Futuristic
|
|
3
|
+
"version": "4.3.0",
|
|
4
|
+
"description": "๐งญ Futuristic TUI workspace navigator & runner - AI-powered project detection for Node, Python, Rust, Go, Java, PHP, Ruby, .NET",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"project-compass": "src/cli.js"
|
|
@@ -17,10 +17,35 @@
|
|
|
17
17
|
"navigator",
|
|
18
18
|
"ink",
|
|
19
19
|
"runner",
|
|
20
|
-
"projects"
|
|
20
|
+
"projects",
|
|
21
|
+
"tui",
|
|
22
|
+
"terminal",
|
|
23
|
+
"workspace",
|
|
24
|
+
"ai",
|
|
25
|
+
"detect",
|
|
26
|
+
"python",
|
|
27
|
+
"rust",
|
|
28
|
+
"node",
|
|
29
|
+
"java",
|
|
30
|
+
"php",
|
|
31
|
+
"ruby",
|
|
32
|
+
"dotnet",
|
|
33
|
+
"uv",
|
|
34
|
+
"fastapi",
|
|
35
|
+
"django",
|
|
36
|
+
"nextjs",
|
|
37
|
+
"react"
|
|
21
38
|
],
|
|
22
39
|
"author": "Satyaa & Clawdy",
|
|
23
40
|
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/CrimsonDevil333333/project-compass"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/CrimsonDevil333333/project-compass",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/CrimsonDevil333333/project-compass/issues"
|
|
48
|
+
},
|
|
24
49
|
"dependencies": {
|
|
25
50
|
"execa": "^9.5.2",
|
|
26
51
|
"fast-glob": "^3.3.3",
|
package/src/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* global setInterval, setTimeout, clearInterval, clearTimeout */
|
|
2
3
|
import React, {useCallback, useEffect, useMemo, useRef, useState, memo} from 'react';
|
|
3
4
|
import {render, Box, Text, useApp, useInput} from 'ink';
|
|
4
5
|
import path from 'path';
|
|
@@ -123,22 +124,38 @@ const OutputPanel = memo(({activeTask, logOffset}) => {
|
|
|
123
124
|
const logWindowEnd = Math.max(0, logs.length - logOffset);
|
|
124
125
|
const visibleLogs = logs.slice(logWindowStart, logWindowEnd);
|
|
125
126
|
|
|
127
|
+
const statusColor = activeTask?.status === 'running' ? 'green' :
|
|
128
|
+
activeTask?.status === 'failed' ? 'red' :
|
|
129
|
+
activeTask?.status === 'killed' ? 'yellow' : 'cyan';
|
|
130
|
+
|
|
131
|
+
const scrollIndicator = logs.length > OUTPUT_WINDOW_SIZE
|
|
132
|
+
? ` โ ${Math.min(logs.length - logOffset, logs.length)}/${logs.length}`
|
|
133
|
+
: '';
|
|
134
|
+
|
|
126
135
|
const logNodes = visibleLogs.length
|
|
127
|
-
? visibleLogs.map((line, i) => create(Text, {key: i}, line))
|
|
128
|
-
: [create(Text, {key: 'empty', dimColor: true}, '
|
|
136
|
+
? visibleLogs.map((line, i) => create(Text, {key: i, wrap: 'truncate'}, line))
|
|
137
|
+
: [create(Text, {key: 'empty', dimColor: true}, 'No logs yet. Run a command to see output here.')];
|
|
129
138
|
|
|
130
139
|
return create(
|
|
131
140
|
Box,
|
|
132
141
|
{
|
|
133
142
|
flexDirection: 'column',
|
|
134
|
-
borderStyle: 'round',
|
|
135
|
-
borderColor:
|
|
136
|
-
|
|
143
|
+
borderStyle: activeTask?.status === 'running' ? 'double' : 'round',
|
|
144
|
+
borderColor: statusColor,
|
|
145
|
+
paddingX: 1,
|
|
146
|
+
paddingY: 0,
|
|
137
147
|
minHeight: OUTPUT_WINDOW_HEIGHT,
|
|
138
148
|
maxHeight: OUTPUT_WINDOW_HEIGHT,
|
|
139
149
|
height: OUTPUT_WINDOW_HEIGHT,
|
|
140
150
|
overflow: 'hidden'
|
|
141
151
|
},
|
|
152
|
+
// Header with log count
|
|
153
|
+
logs.length > 0 && create(
|
|
154
|
+
Box,
|
|
155
|
+
{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 0 },
|
|
156
|
+
create(Text, { dimColor: true, bold: true }, ` ${activeTask?.status?.toUpperCase() || 'IDLE'} `),
|
|
157
|
+
create(Text, { dimColor: true }, `Lines: ${logs.length}${scrollIndicator}`)
|
|
158
|
+
),
|
|
142
159
|
...logNodes
|
|
143
160
|
);
|
|
144
161
|
});
|
|
@@ -164,9 +181,20 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
164
181
|
const [stdinBuffer, setStdinBuffer] = useState('');
|
|
165
182
|
const [stdinCursor, setStdinCursor] = useState(0);
|
|
166
183
|
const [showHelp, setShowHelp] = useState(false);
|
|
184
|
+
const [startup, setStartup] = useState(true);
|
|
185
|
+
const [startupTick, setStartupTick] = useState(0);
|
|
167
186
|
const runningProcessMap = useRef(new Map());
|
|
168
187
|
const lastCommandRef = useRef(null);
|
|
169
188
|
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!startup) return;
|
|
191
|
+
const timer = setInterval(() => {
|
|
192
|
+
setStartupTick(t => t + 1);
|
|
193
|
+
}, 80);
|
|
194
|
+
const hideTimer = setTimeout(() => setStartup(false), 2400);
|
|
195
|
+
return () => { clearInterval(timer); clearTimeout(hideTimer); };
|
|
196
|
+
}, [startup]);
|
|
197
|
+
|
|
170
198
|
const activeTask = useMemo(() => tasks.find(t => t.id === activeTaskId), [tasks, activeTaskId]);
|
|
171
199
|
const running = activeTask?.status === 'running';
|
|
172
200
|
const hasRunningTasks = useMemo(() => tasks.some(t => t.status === 'running'), [tasks]);
|
|
@@ -306,7 +334,7 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
306
334
|
}
|
|
307
335
|
|
|
308
336
|
if (quitConfirm) {
|
|
309
|
-
if (input?.toLowerCase() === 'y') { killAllTasks();
|
|
337
|
+
if (input?.toLowerCase() === 'y') { killAllTasks(); exit(); return; }
|
|
310
338
|
if (input?.toLowerCase() === 'n' || key.escape) { setQuitConfirm(false); return; }
|
|
311
339
|
return;
|
|
312
340
|
}
|
|
@@ -408,26 +436,24 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
408
436
|
const shiftCombo = (char) => key.shift && normalizedInput === char;
|
|
409
437
|
|
|
410
438
|
const clearAndSwitch = (view) => {
|
|
411
|
-
console.clear();
|
|
412
439
|
setMainView(view);
|
|
413
440
|
setViewMode('list');
|
|
414
441
|
setShowHelp(false);
|
|
415
442
|
};
|
|
416
443
|
|
|
417
|
-
if (shiftCombo('h')) {
|
|
418
|
-
if (shiftCombo('s')) {
|
|
444
|
+
if (shiftCombo('h')) { setConfig(prev => { const next = {...prev, showHelpCards: !prev.showHelpCards}; saveConfig(next); return next; }); return; }
|
|
445
|
+
if (shiftCombo('s')) { setConfig(prev => { const next = {...prev, showStructureGuide: !prev.showStructureGuide}; saveConfig(next); return next; }); return; }
|
|
419
446
|
if (shiftCombo('a')) { clearAndSwitch(mainView === 'navigator' ? 'studio' : 'navigator'); return; }
|
|
420
447
|
if (shiftCombo('p')) { clearAndSwitch(mainView === 'navigator' ? 'registry' : 'navigator'); return; }
|
|
421
448
|
if (shiftCombo('n')) { clearAndSwitch(mainView === 'navigator' ? 'architect' : 'navigator'); return; }
|
|
422
449
|
if (shiftCombo('o')) { clearAndSwitch(mainView === 'navigator' ? 'ai' : 'navigator'); return; }
|
|
423
|
-
if (shiftCombo('x')) {
|
|
450
|
+
if (shiftCombo('x')) { setTasks(prev => prev.map(t => t.id === activeTaskId ? {...t, logs: []} : t)); setLogOffset(0); return; }
|
|
424
451
|
if (shiftCombo('e')) { exportLogs(); return; }
|
|
425
|
-
if (shiftCombo('d')) {
|
|
426
|
-
if (shiftCombo('b')) {
|
|
452
|
+
if (shiftCombo('d')) { setActiveTaskId(null); return; }
|
|
453
|
+
if (shiftCombo('b')) { setConfig(prev => { const next = {...prev, showArtBoard: !prev.showArtBoard}; saveConfig(next); return next; }); return; }
|
|
427
454
|
|
|
428
455
|
if (shiftCombo('t')) {
|
|
429
456
|
setMainView((prev) => {
|
|
430
|
-
console.clear();
|
|
431
457
|
if (prev === 'tasks') return 'navigator';
|
|
432
458
|
if (tasks.length > 0 && !activeTaskId) setActiveTaskId(tasks[0].id);
|
|
433
459
|
return 'tasks';
|
|
@@ -502,7 +528,6 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
502
528
|
const next = prev - pageLimit;
|
|
503
529
|
return next < 0 ? 0 : next;
|
|
504
530
|
});
|
|
505
|
-
console.clear();
|
|
506
531
|
return;
|
|
507
532
|
}
|
|
508
533
|
|
|
@@ -516,24 +541,22 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
516
541
|
}
|
|
517
542
|
return next;
|
|
518
543
|
});
|
|
519
|
-
console.clear();
|
|
520
544
|
return;
|
|
521
545
|
}
|
|
522
546
|
|
|
523
547
|
|
|
524
|
-
if (normalizedInput === '?') {
|
|
548
|
+
if (normalizedInput === '?') { setShowHelp((prev) => !prev); return; }
|
|
525
549
|
if (shiftCombo('l') && lastCommandRef.current) { runProjectCommand(lastCommandRef.current.commandMeta, lastCommandRef.current.project); return; }
|
|
526
550
|
|
|
527
|
-
if (key.upArrow && !key.shift && projects.length > 0) {
|
|
528
|
-
if (key.downArrow && !key.shift && projects.length > 0) {
|
|
551
|
+
if (key.upArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev - 1 + projects.length) % projects.length); return; }
|
|
552
|
+
if (key.downArrow && !key.shift && projects.length > 0) { setSelectedIndex((prev) => (prev + 1) % projects.length); return; }
|
|
529
553
|
if (key.return) {
|
|
530
554
|
if (!selectedProject) return;
|
|
531
|
-
console.clear();
|
|
532
555
|
setViewMode((prev) => (prev === 'detail' ? 'list' : 'detail'));
|
|
533
556
|
return;
|
|
534
557
|
}
|
|
535
558
|
if (shiftCombo('q') || isCtrlC) {
|
|
536
|
-
if (hasRunningTasks) setQuitConfirm(true); else {
|
|
559
|
+
if (hasRunningTasks) setQuitConfirm(true); else { exit(); }
|
|
537
560
|
return;
|
|
538
561
|
}
|
|
539
562
|
|
|
@@ -630,9 +653,27 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
630
653
|
case 'registry': return create(PackageRegistry, {selectedProject, projects, onRunCommand: runProjectCommand, CursorText, onSelectProject: (idx) => setSelectedIndex(idx)});
|
|
631
654
|
case 'architect': return create(ProjectArchitect, {rootPath, onRunCommand: runProjectCommand, CursorText, onReturn: () => setMainView('navigator')});
|
|
632
655
|
case 'ai': return create(AIHorizon, {rootPath, selectedProject, onRunCommand: runProjectCommand, CursorText, config, setConfig, saveConfig});
|
|
633
|
-
|
|
656
|
+
default: {
|
|
657
|
+
const quickActions = selectedProject ? [
|
|
658
|
+
{ key: 'B', label: 'Build', color: 'magenta' },
|
|
659
|
+
{ key: 'T', label: 'Test', color: 'cyan' },
|
|
660
|
+
{ key: 'R', label: 'Run', color: 'green' },
|
|
661
|
+
{ key: 'I', label: 'Install', color: 'blue' },
|
|
662
|
+
{ key: '0', label: 'AI', color: 'yellow' }
|
|
663
|
+
] : [];
|
|
664
|
+
|
|
634
665
|
const navigatorBody = [
|
|
635
666
|
create(Header, {projectCountLabel, rootPath, running, statusHint, toggleHint, orbitHint, artHint}),
|
|
667
|
+
// Quick Actions Bar
|
|
668
|
+
quickActions.length > 0 && create(Box, {key: 'quick-actions', flexDirection: 'row', marginBottom: 0, paddingX: 1, paddingY: 0, borderStyle: 'single', borderColor: 'cyan'},
|
|
669
|
+
create(Text, {dimColor: true}, 'Quick: '),
|
|
670
|
+
...quickActions.map((action, idx) =>
|
|
671
|
+
create(Text, {key: action.key, color: action.color},
|
|
672
|
+
`${idx > 0 ? ' ยท ' : ''}[${action.key}] ${action.label}`
|
|
673
|
+
)
|
|
674
|
+
),
|
|
675
|
+
create(Text, {dimColor: true}, ' ยท [?] Help')
|
|
676
|
+
),
|
|
636
677
|
config.showArtBoard && create(Box, {key: 'artboard', flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: 'gray', padding: 1},
|
|
637
678
|
create(Box, {flexDirection: 'row', justifyContent: 'space-between'}, create(Text, {color: 'magenta', bold: true}, 'Art-coded build atlas'), create(Text, {dimColor: true}, 'press ? for overlay help')),
|
|
638
679
|
create(Box, {flexDirection: 'row', marginTop: 1}, ...ART_CHARS.map((char, i) => create(Text, {key: i, color: ART_COLORS[i % ART_COLORS.length]}, char.repeat(2)))),
|
|
@@ -663,6 +704,35 @@ function Compass({rootPath, initialView = 'navigator'}) {
|
|
|
663
704
|
}
|
|
664
705
|
};
|
|
665
706
|
|
|
707
|
+
// Startup banner with animation
|
|
708
|
+
if (startup) {
|
|
709
|
+
const frame = startupTick % 240;
|
|
710
|
+
const progress = Math.min(frame / 240 * 100, 100);
|
|
711
|
+
const barLength = 30;
|
|
712
|
+
const filled = Math.floor(barLength * progress / 100);
|
|
713
|
+
const bar = 'โ'.repeat(filled) + 'โ'.repeat(barLength - filled);
|
|
714
|
+
const spinner = ['โ ', 'โ ', 'โ น', 'โ ธ', 'โ ผ', 'โ ด', 'โ ฆ', 'โ ง', 'โ ', 'โ '][startupTick % 10];
|
|
715
|
+
|
|
716
|
+
return create(Box, {flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%'},
|
|
717
|
+
create(Box, {marginBottom: 1},
|
|
718
|
+
create(Text, {color: 'magenta', bold: true}, '๐งญ '),
|
|
719
|
+
create(Text, {color: 'cyan', bold: true}, 'PROJECT COMPASS'),
|
|
720
|
+
create(Text, {color: 'magenta', bold: true}, ' ๐งญ')
|
|
721
|
+
),
|
|
722
|
+
create(Box, {marginBottom: 1},
|
|
723
|
+
create(Text, {dimColor: true}, 'Initializing command center...')
|
|
724
|
+
),
|
|
725
|
+
create(Box, {marginBottom: 1},
|
|
726
|
+
create(Text, {color: 'cyan'}, bar),
|
|
727
|
+
create(Text, {dimColor: true}, ` ${Math.floor(progress)}%`)
|
|
728
|
+
),
|
|
729
|
+
create(Box, {},
|
|
730
|
+
create(Text, {color: 'yellow'}, ` ${spinner} `),
|
|
731
|
+
create(Text, {dimColor: true}, 'Scanning projects...')
|
|
732
|
+
)
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
666
736
|
return create(Box, {flexDirection: 'column', padding: 1, width: '100%'}, renderView());
|
|
667
737
|
}
|
|
668
738
|
|
package/src/components/Footer.js
CHANGED
|
@@ -1,25 +1,81 @@
|
|
|
1
|
-
|
|
1
|
+
/* global setInterval, clearInterval */
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
2
3
|
import { Box, Text } from 'ink';
|
|
3
4
|
|
|
5
|
+
const PULSE = ['โฑ', 'โฐ', 'โฑ'];
|
|
6
|
+
const ARROW = ['โ', 'โ', 'โจ', 'โ'];
|
|
7
|
+
|
|
4
8
|
export default function Footer({ toggleHint, running, stdinBuffer, stdinCursor, CursorText }) {
|
|
9
|
+
const [pulse, setPulse] = useState(0);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!running) return;
|
|
13
|
+
const timer = setInterval(() => setPulse(p => p + 1), 500);
|
|
14
|
+
return () => clearInterval(timer);
|
|
15
|
+
}, [running]);
|
|
16
|
+
|
|
17
|
+
const pulseChar = running ? PULSE[pulse % PULSE.length] : '';
|
|
18
|
+
const arrow = running ? ARROW[pulse % ARROW.length] : 'โ';
|
|
19
|
+
|
|
5
20
|
return React.createElement(
|
|
6
21
|
Box,
|
|
7
22
|
{ flexDirection: 'column', marginTop: 1 },
|
|
23
|
+
// Status bar
|
|
8
24
|
React.createElement(
|
|
9
25
|
Box,
|
|
10
|
-
{ flexDirection: 'row', justifyContent: 'space-between' },
|
|
11
|
-
React.createElement(
|
|
12
|
-
|
|
26
|
+
{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 0 },
|
|
27
|
+
React.createElement(
|
|
28
|
+
Box,
|
|
29
|
+
{ flexDirection: 'row', alignItems: 'center' },
|
|
30
|
+
React.createElement(Text, { dimColor: true }, `${arrow} `),
|
|
31
|
+
React.createElement(Text, { dimColor: true },
|
|
32
|
+
running ? 'Type to feed stdin ยท Enter: submit' : 'Run a command or press Shift+T for tasks'
|
|
33
|
+
)
|
|
34
|
+
),
|
|
35
|
+
React.createElement(
|
|
36
|
+
Box,
|
|
37
|
+
{ flexDirection: 'row', alignItems: 'center' },
|
|
38
|
+
React.createElement(Text, { dimColor: true }, `${toggleHint}`),
|
|
39
|
+
React.createElement(Text, { dimColor: true }, ` ยท `),
|
|
40
|
+
React.createElement(Text, { dimColor: true }, `Shift+S: Guide`)
|
|
41
|
+
)
|
|
13
42
|
),
|
|
43
|
+
// Input area
|
|
14
44
|
React.createElement(
|
|
15
45
|
Box,
|
|
16
|
-
{
|
|
17
|
-
|
|
46
|
+
{
|
|
47
|
+
marginTop: 0,
|
|
48
|
+
flexDirection: 'row',
|
|
49
|
+
borderStyle: running ? 'double' : 'round',
|
|
50
|
+
borderColor: running ? 'green' : 'gray',
|
|
51
|
+
paddingX: 1,
|
|
52
|
+
paddingY: 0
|
|
53
|
+
},
|
|
54
|
+
React.createElement(
|
|
55
|
+
Text,
|
|
56
|
+
{ bold: true, color: running ? 'green' : 'white' },
|
|
57
|
+
running ? ` ${pulseChar} Stdin ` : ' โ Input '
|
|
58
|
+
),
|
|
18
59
|
React.createElement(
|
|
19
60
|
Box,
|
|
20
|
-
{ marginLeft: 1 },
|
|
21
|
-
React.createElement(CursorText, {
|
|
61
|
+
{ marginLeft: 1, flexGrow: 1 },
|
|
62
|
+
React.createElement(CursorText, {
|
|
63
|
+
value: stdinBuffer || (running ? '' : 'Ready for commandsโฆ'),
|
|
64
|
+
cursorIndex: stdinCursor,
|
|
65
|
+
active: running
|
|
66
|
+
})
|
|
67
|
+
),
|
|
68
|
+
running && React.createElement(
|
|
69
|
+
Text,
|
|
70
|
+
{ color: 'green', bold: true },
|
|
71
|
+
' [Active]'
|
|
22
72
|
)
|
|
73
|
+
),
|
|
74
|
+
// Separator
|
|
75
|
+
React.createElement(
|
|
76
|
+
Box,
|
|
77
|
+
{ marginTop: 0 },
|
|
78
|
+
React.createElement(Text, { dimColor: true }, 'โ'.repeat(50))
|
|
23
79
|
)
|
|
24
80
|
);
|
|
25
81
|
}
|
package/src/components/Header.js
CHANGED
|
@@ -1,21 +1,60 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import path from 'path';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
const SPARKS = ['โฆ', 'โง', 'โฉ', 'โช', 'โซ'];
|
|
6
|
+
|
|
7
|
+
export default function Header({ projectCountLabel, rootPath, running, toggleHint, orbitHint }) {
|
|
8
|
+
const time = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
9
|
+
const spark = SPARKS[Math.floor(Date.now() / 1000) % SPARKS.length];
|
|
10
|
+
|
|
5
11
|
return React.createElement(
|
|
6
12
|
Box,
|
|
7
|
-
{
|
|
13
|
+
{ flexDirection: 'column', marginBottom: 1 },
|
|
14
|
+
// Top bar with logo and status
|
|
15
|
+
React.createElement(
|
|
16
|
+
Box,
|
|
17
|
+
{ justifyContent: 'space-between', alignItems: 'center' },
|
|
18
|
+
React.createElement(
|
|
19
|
+
Box,
|
|
20
|
+
{ flexDirection: 'row', alignItems: 'center' },
|
|
21
|
+
React.createElement(Text, { color: 'magenta', bold: true }, '๐งญ '),
|
|
22
|
+
React.createElement(Text, { color: 'magenta', bold: true }, 'PROJECT COMPASS'),
|
|
23
|
+
React.createElement(Text, { color: 'cyan' }, ` ${spark}`)
|
|
24
|
+
),
|
|
25
|
+
React.createElement(
|
|
26
|
+
Box,
|
|
27
|
+
{ flexDirection: 'row', alignItems: 'center' },
|
|
28
|
+
React.createElement(Text, {
|
|
29
|
+
color: running ? 'yellow' : 'green',
|
|
30
|
+
bold: true
|
|
31
|
+
}, running ? 'โก ACTIVE' : 'โ IDLE'),
|
|
32
|
+
React.createElement(Text, { dimColor: true }, ` ${time}`)
|
|
33
|
+
)
|
|
34
|
+
),
|
|
35
|
+
// Info bar
|
|
8
36
|
React.createElement(
|
|
9
37
|
Box,
|
|
10
|
-
{
|
|
11
|
-
React.createElement(
|
|
12
|
-
|
|
38
|
+
{ justifyContent: 'space-between', marginTop: 0 },
|
|
39
|
+
React.createElement(
|
|
40
|
+
Box,
|
|
41
|
+
{ flexDirection: 'row' },
|
|
42
|
+
React.createElement(Text, { dimColor: true }, `${spark} `),
|
|
43
|
+
React.createElement(Text, { color: 'cyan' }, projectCountLabel),
|
|
44
|
+
React.createElement(Text, { dimColor: true }, ' in '),
|
|
45
|
+
React.createElement(Text, { color: 'white' }, path.basename(rootPath))
|
|
46
|
+
),
|
|
47
|
+
React.createElement(
|
|
48
|
+
Box,
|
|
49
|
+
{ flexDirection: 'row' },
|
|
50
|
+
React.createElement(Text, { dimColor: true }, `${toggleHint} ยท ${orbitHint}`)
|
|
51
|
+
)
|
|
13
52
|
),
|
|
53
|
+
// Separator
|
|
14
54
|
React.createElement(
|
|
15
55
|
Box,
|
|
16
|
-
{
|
|
17
|
-
React.createElement(Text, {
|
|
18
|
-
React.createElement(Text, { dimColor: true }, `${toggleHint} ยท ${orbitHint} ยท ${artHint} ยท Shift+Q: Quit`)
|
|
56
|
+
{ marginTop: 0 },
|
|
57
|
+
React.createElement(Text, { dimColor: true }, 'โ'.repeat(50))
|
|
19
58
|
)
|
|
20
59
|
);
|
|
21
60
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/* global setInterval, clearInterval */
|
|
2
|
+
import React, { useMemo, useState, useEffect } from 'react';
|
|
2
3
|
import { Box, Text } from 'ink';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
|
|
6
|
+
const SPINNER = ['โ ', 'โ ', 'โ น', 'โ ธ', 'โ ผ', 'โ ด', 'โ ฆ', 'โ ง', 'โ ', 'โ '];
|
|
7
|
+
const SELECTION_BAR = 'โ';
|
|
8
|
+
|
|
5
9
|
export default function Navigator({
|
|
6
10
|
projects,
|
|
7
11
|
selectedIndex,
|
|
@@ -10,39 +14,89 @@ export default function Navigator({
|
|
|
10
14
|
error,
|
|
11
15
|
maxVisibleProjects = 3
|
|
12
16
|
}) {
|
|
17
|
+
const [tick, setTick] = useState(0);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!loading) return;
|
|
21
|
+
const timer = setInterval(() => setTick(t => t + 1), 100);
|
|
22
|
+
return () => clearInterval(timer);
|
|
23
|
+
}, [loading]);
|
|
24
|
+
|
|
13
25
|
const page = Math.floor(selectedIndex / maxVisibleProjects);
|
|
14
26
|
const start = page * maxVisibleProjects;
|
|
15
27
|
const end = start + maxVisibleProjects;
|
|
16
28
|
const visibleProjects = projects.slice(start, end);
|
|
17
|
-
|
|
29
|
+
|
|
18
30
|
const projectRows = useMemo(() => {
|
|
19
|
-
if (loading)
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
if (loading) {
|
|
32
|
+
const spinner = SPINNER[tick % SPINNER.length];
|
|
33
|
+
return [React.createElement(
|
|
34
|
+
Box,
|
|
35
|
+
{ key: 'scanning', flexDirection: 'row', alignItems: 'center' },
|
|
36
|
+
React.createElement(Text, { color: 'cyan' }, ` ${spinner} `),
|
|
37
|
+
React.createElement(Text, { dimColor: true }, 'Scanning projectsโฆ'),
|
|
38
|
+
React.createElement(Text, { color: 'cyan' }, ' โ'.repeat(3 + (tick % 3)))
|
|
39
|
+
)];
|
|
40
|
+
}
|
|
41
|
+
if (error) return [React.createElement(
|
|
42
|
+
Box,
|
|
43
|
+
{ key: 'error', flexDirection: 'row', alignItems: 'center' },
|
|
44
|
+
React.createElement(Text, { color: 'red', bold: true }, ' โ '),
|
|
45
|
+
React.createElement(Text, { color: 'red' }, `Unable to scan: ${error}`)
|
|
46
|
+
)];
|
|
47
|
+
if (projects.length === 0) return [React.createElement(
|
|
48
|
+
Box,
|
|
49
|
+
{ key: 'empty', flexDirection: 'row', alignItems: 'center' },
|
|
50
|
+
React.createElement(Text, { color: 'yellow' }, ' โ '),
|
|
51
|
+
React.createElement(Text, { dimColor: true }, 'No recognizable project manifests found.')
|
|
52
|
+
)];
|
|
22
53
|
|
|
23
54
|
return visibleProjects.map((project, index) => {
|
|
24
55
|
const absoluteIndex = start + index;
|
|
25
56
|
const isSelected = absoluteIndex === selectedIndex;
|
|
26
|
-
const frameworkBadges = (project.frameworks || []).map((frame) =>
|
|
57
|
+
const frameworkBadges = (project.frameworks || []).map((frame) =>
|
|
58
|
+
React.createElement(Text, { key: frame.name, color: 'cyan', dimColor: !isSelected }, ` ${frame.icon}${frame.name}`)
|
|
59
|
+
);
|
|
27
60
|
const hasMissingRuntime = project.missingBinaries && project.missingBinaries.length > 0;
|
|
28
61
|
|
|
29
62
|
return React.createElement(
|
|
30
63
|
Box,
|
|
31
|
-
{
|
|
64
|
+
{
|
|
65
|
+
key: project.id,
|
|
66
|
+
flexDirection: 'column',
|
|
67
|
+
marginBottom: 0,
|
|
68
|
+
borderStyle: isSelected ? 'bold' : 'single',
|
|
69
|
+
borderColor: isSelected ? 'cyan' : 'gray',
|
|
70
|
+
paddingX: 1,
|
|
71
|
+
paddingY: 0
|
|
72
|
+
},
|
|
32
73
|
React.createElement(
|
|
33
74
|
Box,
|
|
34
|
-
{ flexDirection: 'row' },
|
|
35
|
-
React.createElement(Text, {
|
|
36
|
-
|
|
75
|
+
{ flexDirection: 'row', alignItems: 'center' },
|
|
76
|
+
React.createElement(Text, {
|
|
77
|
+
color: isSelected ? 'cyan' : 'white',
|
|
78
|
+
bold: isSelected
|
|
79
|
+
}, `${isSelected ? SELECTION_BAR + ' ' : ' '}${project.icon} ${project.name}`),
|
|
80
|
+
hasMissingRuntime && React.createElement(Text, { color: 'red', bold: true }, ' โ Runtime missing')
|
|
37
81
|
),
|
|
38
|
-
React.createElement(
|
|
39
|
-
|
|
82
|
+
React.createElement(
|
|
83
|
+
Box,
|
|
84
|
+
{ flexDirection: 'row', alignItems: 'center' },
|
|
85
|
+
React.createElement(Text, { dimColor: true }, ` ${project.type}`),
|
|
86
|
+
React.createElement(Text, { dimColor: true }, ` ยท ${path.relative(rootPath, project.path) || '.'}`)
|
|
87
|
+
),
|
|
88
|
+
frameworkBadges.length > 0 && React.createElement(
|
|
89
|
+
Box,
|
|
90
|
+
{ marginTop: 0 },
|
|
91
|
+
React.createElement(Text, { dimColor: true }, ' '),
|
|
92
|
+
...frameworkBadges
|
|
93
|
+
)
|
|
40
94
|
);
|
|
41
95
|
});
|
|
42
|
-
}, [loading, error, projects.length, visibleProjects, selectedIndex, start, rootPath]);
|
|
96
|
+
}, [loading, error, projects.length, visibleProjects, selectedIndex, start, rootPath, tick]);
|
|
43
97
|
|
|
44
98
|
const totalPages = Math.ceil(projects.length / maxVisibleProjects);
|
|
45
|
-
|
|
99
|
+
|
|
46
100
|
return React.createElement(
|
|
47
101
|
Box,
|
|
48
102
|
{ flexDirection: 'column' },
|
|
@@ -50,7 +104,7 @@ export default function Navigator({
|
|
|
50
104
|
projects.length > maxVisibleProjects && React.createElement(
|
|
51
105
|
Box,
|
|
52
106
|
{ marginTop: 1, justifyContent: 'center' },
|
|
53
|
-
React.createElement(Text, { dimColor: true },
|
|
107
|
+
React.createElement(Text, { dimColor: true }, `${'-'.repeat(3)} Page ${page + 1}/${totalPages} (${projects.length} projects) ${'-'.repeat(3)}`)
|
|
54
108
|
)
|
|
55
109
|
);
|
|
56
110
|
}
|
package/src/detectors/dotnet.js
CHANGED
|
@@ -1,13 +1,118 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
1
2
|
import path from 'path';
|
|
2
|
-
import { checkBinary } from './utils.js';
|
|
3
|
+
import { checkBinary, hasProjectFile } from './utils.js';
|
|
4
|
+
|
|
5
|
+
function parseCsProj(content) {
|
|
6
|
+
const metadata = {
|
|
7
|
+
name: '',
|
|
8
|
+
targetFramework: '',
|
|
9
|
+
dependencies: []
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const nameMatch = content.match(/<AssemblyName>([^<]+)<\/AssemblyName>/);
|
|
13
|
+
if (nameMatch) metadata.name = nameMatch[1];
|
|
14
|
+
|
|
15
|
+
const frameworkMatch = content.match(/<TargetFramework>([^<]+)<\/TargetFramework>/);
|
|
16
|
+
if (frameworkMatch) metadata.targetFramework = frameworkMatch[1];
|
|
17
|
+
|
|
18
|
+
const packageMatches = content.matchAll(/<PackageReference\s+Include="([^"]+)"[^/]*\/?>/g);
|
|
19
|
+
for (const match of packageMatches) {
|
|
20
|
+
if (match[1]) metadata.dependencies.push(match[1]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return metadata;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function detectDotnetFrameworks(deps) {
|
|
27
|
+
const frameworks = [];
|
|
28
|
+
const depStr = deps.join(' ').toLowerCase();
|
|
29
|
+
|
|
30
|
+
if (depStr.includes('microsoft.aspnetcore') || depStr.includes('microsoft.aspnetcore.app')) frameworks.push({ name: 'ASP.NET Core', icon: '๐ท' });
|
|
31
|
+
if (depStr.includes('blazor')) frameworks.push({ name: 'Blazor', icon: '๐' });
|
|
32
|
+
if (depStr.includes('entityframework') || depStr.includes('efcore')) frameworks.push({ name: 'Entity Framework', icon: '๐๏ธ' });
|
|
33
|
+
if (depStr.includes('newtonsoft.json')) frameworks.push({ name: 'Newtonsoft.Json', icon: '๐' });
|
|
34
|
+
if (depStr.includes('xunit')) frameworks.push({ name: 'xUnit', icon: 'โ
' });
|
|
35
|
+
if (depStr.includes('nunit')) frameworks.push({ name: 'NUnit', icon: '๐ฌ' });
|
|
36
|
+
if (depStr.includes('mstest')) frameworks.push({ name: 'MSTest', icon: '๐งช' });
|
|
37
|
+
if (depStr.includes('automapper')) frameworks.push({ name: 'AutoMapper', icon: '๐' });
|
|
38
|
+
if (depStr.includes('mass transit') || depStr.includes('masstransit')) frameworks.push({ name: 'MassTransit', icon: '๐' });
|
|
39
|
+
if (depStr.includes('grpc')) frameworks.push({ name: 'gRPC', icon: '๐' });
|
|
40
|
+
|
|
41
|
+
return frameworks;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findCsProj(projectPath) {
|
|
45
|
+
try {
|
|
46
|
+
const files = fs.readdirSync(projectPath);
|
|
47
|
+
const csproj = files.find(f => f.endsWith('.csproj') || f.endsWith('.fsproj'));
|
|
48
|
+
if (csproj) return path.join(projectPath, csproj);
|
|
49
|
+
} catch { /* ignore */ }
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
3
53
|
export default {
|
|
4
|
-
type: 'dotnet',
|
|
54
|
+
type: 'dotnet',
|
|
55
|
+
label: '.NET',
|
|
56
|
+
icon: '๐ฏ',
|
|
57
|
+
priority: 65,
|
|
58
|
+
files: ['*.csproj', '*.sln', '*.fsproj'],
|
|
59
|
+
binaries: ['dotnet'],
|
|
5
60
|
async build(projectPath, manifest) {
|
|
6
61
|
const missingBinaries = this.binaries.filter(b => !checkBinary(b));
|
|
62
|
+
let metadata = { name: '', targetFramework: '', dependencies: [] };
|
|
63
|
+
let frameworks = [];
|
|
64
|
+
|
|
65
|
+
const csprojPath = findCsProj(projectPath);
|
|
66
|
+
if (csprojPath && fs.existsSync(csprojPath)) {
|
|
67
|
+
const content = fs.readFileSync(csprojPath, 'utf-8');
|
|
68
|
+
metadata = parseCsProj(content);
|
|
69
|
+
frameworks = detectDotnetFrameworks(metadata.dependencies);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const commands = {
|
|
73
|
+
install: { label: 'Dotnet restore', command: ['dotnet', 'restore'], source: 'builtin' },
|
|
74
|
+
build: { label: 'Dotnet build', command: ['dotnet', 'build'], source: 'builtin' },
|
|
75
|
+
test: { label: 'Dotnet test', command: ['dotnet', 'test'], source: 'builtin' },
|
|
76
|
+
run: { label: 'Dotnet run', command: ['dotnet', 'run'], source: 'builtin' },
|
|
77
|
+
clean: { label: 'Dotnet clean', command: ['dotnet', 'clean'], source: 'builtin' },
|
|
78
|
+
publish: { label: 'Dotnet publish', command: ['dotnet', 'publish'], source: 'builtin' }
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (hasProjectFile(projectPath, '*.sln')) {
|
|
82
|
+
commands['restore-sl'] = { label: 'Restore Solution', command: ['dotnet', 'restore'], source: 'builtin' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const setupHints = [];
|
|
86
|
+
if (missingBinaries.length > 0) {
|
|
87
|
+
setupHints.push('Install .NET SDK: https://dot.net/');
|
|
88
|
+
}
|
|
89
|
+
if (metadata.targetFramework) {
|
|
90
|
+
setupHints.push(`Target Framework: ${metadata.targetFramework}`);
|
|
91
|
+
}
|
|
92
|
+
if (metadata.dependencies.length > 0) {
|
|
93
|
+
setupHints.push('Run dotnet restore to fetch dependencies');
|
|
94
|
+
}
|
|
95
|
+
|
|
7
96
|
return {
|
|
8
|
-
id: `${projectPath}::dotnet`,
|
|
9
|
-
|
|
10
|
-
|
|
97
|
+
id: `${projectPath}::dotnet`,
|
|
98
|
+
path: projectPath,
|
|
99
|
+
name: metadata.name || path.basename(projectPath),
|
|
100
|
+
type: '.NET',
|
|
101
|
+
icon: '๐ฏ',
|
|
102
|
+
priority: this.priority,
|
|
103
|
+
commands,
|
|
104
|
+
metadata: {
|
|
105
|
+
...metadata,
|
|
106
|
+
packageManager: 'dotnet'
|
|
107
|
+
},
|
|
108
|
+
manifest: path.basename(manifest),
|
|
109
|
+
description: frameworks.map(f => f.name).join(', ') || metadata.targetFramework,
|
|
110
|
+
missingBinaries,
|
|
111
|
+
frameworks,
|
|
112
|
+
extra: {
|
|
113
|
+
setupHints,
|
|
114
|
+
targetFramework: metadata.targetFramework
|
|
115
|
+
}
|
|
11
116
|
};
|
|
12
117
|
}
|
|
13
118
|
};
|